@lincoln504/pi-research 1.3.8 → 1.3.10

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/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/Lincoln504/pi-research/main/docs/media/banner.jpg" alt="pi-research: free unlimited web search and knowledge store for coding agents" width="100%" />
2
+ <img src="https://raw.githubusercontent.com/Lincoln504/pi-research/main/docs/media/banner.jpg" alt="pi-research: free unlimited web search and knowledge store for agents" width="100%" />
3
3
  </p>
4
4
 
5
5
  <p align="center">
6
- <strong>Free unlimited web search &amp; knowledge store for coding agents</strong>
6
+ <strong>Free unlimited web search &amp; knowledge store for agents</strong>
7
7
  </p>
8
8
 
9
9
  <p align="center">
@@ -31,7 +31,7 @@ Search and scraping run locally through a stealth browser with no search provide
31
31
 
32
32
  ### Install
33
33
 
34
- As a [pi](https://github.com/earendil-works/pi) extension:
34
+ As a [Pi](https://github.com/earendil-works/pi) Coding Agent extension:
35
35
 
36
36
  ```bash
37
37
  pi install npm:@lincoln504/pi-research
@@ -44,7 +44,7 @@ npm install -g @lincoln504/pi-research
44
44
  pi-research skill install
45
45
  ```
46
46
 
47
- In pi it works out of the box on your session's model and pi's configuration. Standalone use (agent skill, CLI, or [SDK](docs/SDK.md)) needs a model configured. See [Configuration](docs/CONFIGURATION.md). The first install pulls the stealth browser engine, which takes a few minutes.
47
+ In pi it works out of the box on your session's model and pi's configuration. Standalone use ([agent skill](docs/AGENT-SKILL.md) or [SDK](docs/SDK.md)) needs a model configured. See [Configuration](docs/CONFIGURATION.md). The first install pulls the stealth browser engine, which takes a few minutes.
48
48
 
49
49
  ### How it works
50
50
 
@@ -56,14 +56,14 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
56
56
 
57
57
  ### Use cases
58
58
 
59
- - Researching for your [Pi](https://github.com/earendil-works/pi) Coding Agent. Plug and play with no API key needed.
59
+ - Researching for your pi. Plug and play with no API key needed.
60
60
  - Researching from Claude Code, Codex, OpenClaw, or another coding agent while a cheaper or local model drives the run so it doesn't spend your main agent's budget.
61
61
  - Keeping a persistent and searchable knowledge store of findings, scoped globally or per project.
62
62
  - Building agent systems that find and read web content or populating a dataset of web sources.
63
63
 
64
64
  ### Why pi-research
65
65
 
66
- - **No quota and no monthly fee.** Most AI search providers cap free searches and then charge, with results served from *their* index on *their* servers. pi-research searches and scrapes locally through DuckDuckGo in a stealth browser.
66
+ - **No quota and no monthly fee.** Most AI search providers cap free searches and then charge, with results served from their index on their servers. pi-research searches and scrapes locally through DuckDuckGo in a stealth browser.
67
67
  - **You own your index.** Every finding is written to a local [LanceDB](https://lancedb.com) knowledge store that seeds future runs, so repeat questions get faster and cheaper answers.
68
68
  - **Cited reports rather than raw dumps.** You get one sourced Markdown report instead of raw web content pasted into the conversation. Context efficient and easy to verify.
69
69
  - **Read-only by design.** The research agent cannot run shell commands or write, edit, or delete anything. It can read and search files, so treat it as you would any read-only agent with access to your machine.
@@ -74,7 +74,7 @@ Three depth levels (normal, deep, ultra) set the team size and number of rounds.
74
74
  - Node.js >= 22.19.0
75
75
  - An LLM with a 100k+ context window (bring your own key or use a local model)
76
76
  - Internet access on a residential IP, since search, scraping, and YouTube transcripts get bot-blocked from datacenter/VPS/cloud IPs
77
- - The [pi runtime](https://github.com/earendil-works/pi). The pi extension uses the host's copy while the standalone CLI and agent skill install it as a dependency.
77
+ - The pi runtime. The pi extension uses the host's copy while the standalone CLI and agent skill install it as a dependency.
78
78
  - Cloudflare and similar systems block scraping on some sites. A run flags pages it can't reach and compensates with a wide pool of search results to cite.
79
79
 
80
80
  ### Channels
package/dist/cli.mjs CHANGED
@@ -247,22 +247,36 @@ function redactSecrets(message) {
247
247
  function stripTerminalEscapes(message) {
248
248
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
249
249
  }
250
- var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
250
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
251
251
  var init_log_utils = __esm({
252
252
  "src/utils/log-utils.ts"() {
253
253
  "use strict";
254
254
  logContextStorage = new AsyncLocalStorage2();
255
255
  MAX_LOG_MESSAGE_LENGTH = 1e4;
256
256
  ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
257
- URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
258
- COOKIE_HEADER_PATTERN = /\b(set[_-]?cookie|cookie)\b(["']?\s*[:=]\s*["']?)([^\r\n]+)/gi;
259
- SENSITIVE_KV_PATTERN = /\b(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)\b(["']?\s*[:=]\s*["']?)([^\s"',&)]+)/gi;
260
- KNOWN_TOKEN_PATTERN = /\b(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
261
- JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
262
- BASIC_AUTH_PATTERN = /\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}/gi;
263
- BEARER_AUTH_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
264
- PROVIDER_TOKEN_PATTERN = /\b(AIza[0-9A-Za-z_-]{35}|ya29\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})\b/g;
265
- LONG_HEX_SECRET_PATTERN = /\b[0-9a-fA-F]{32,}\b/g;
257
+ URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,31}:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
258
+ B_LEFT = "(?<![A-Za-z0-9])";
259
+ B_RIGHT = "(?![A-Za-z0-9])";
260
+ COOKIE_HEADER_PATTERN = new RegExp(
261
+ `${B_LEFT}(set[_-]?cookie|cookie)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\r\\n]+)`,
262
+ "gi"
263
+ );
264
+ SENSITIVE_KV_PATTERN = new RegExp(
265
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
266
+ "gi"
267
+ );
268
+ KNOWN_TOKEN_PATTERN = new RegExp(
269
+ `${B_LEFT}(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})${B_RIGHT}`,
270
+ "g"
271
+ );
272
+ JWT_PATTERN = new RegExp(`${B_LEFT}eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+`, "g");
273
+ BASIC_AUTH_PATTERN = new RegExp(`${B_LEFT}Basic\\s+[A-Za-z0-9+/]{16,}={0,2}`, "gi");
274
+ BEARER_AUTH_PATTERN = new RegExp(`${B_LEFT}Bearer\\s+[A-Za-z0-9._~+/-]+=*`, "gi");
275
+ PROVIDER_TOKEN_PATTERN = new RegExp(
276
+ `${B_LEFT}(AIza[0-9A-Za-z_-]{35}|ya29\\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})${B_RIGHT}`,
277
+ "g"
278
+ );
279
+ LONG_HEX_SECRET_PATTERN = new RegExp(`${B_LEFT}[0-9a-fA-F]{32,}${B_RIGHT}`, "g");
266
280
  CONTROL_CHARS_PATTERN = /[\x00-\x1f\x7f]/g;
267
281
  REDACT_SCAN_LENGTH = MAX_LOG_MESSAGE_LENGTH * 4;
268
282
  TOKEN_CHAR_PATTERN = /[A-Za-z0-9_\-./+=]/;
@@ -2648,6 +2662,7 @@ function saveConfig(config, scope = "local", cwd = process.cwd(), changedKeys) {
2648
2662
  PI_RESEARCH_LLM_THINKING_LEVEL: config.LLM_THINKING_LEVEL,
2649
2663
  PI_RESEARCH_PLANNING_MAX_TOKENS: String(config.PLANNING_MAX_TOKENS),
2650
2664
  PI_RESEARCH_SYNTHESIS_MAX_TOKENS: String(config.SYNTHESIS_MAX_TOKENS),
2665
+ PI_RESEARCH_RESEARCHER_MAX_TOKENS: String(config.RESEARCHER_MAX_TOKENS),
2651
2666
  PI_RESEARCH_MIGRATION_STRATEGY: config.MIGRATION_STRATEGY,
2652
2667
  PI_RESEARCH_CONSOLE_LOG: String(config.CONSOLE_LOG),
2653
2668
  ...config.RESEARCH_MODEL ? { PI_RESEARCH_MODEL: config.RESEARCH_MODEL } : {},
@@ -2812,6 +2827,7 @@ function createConfig(env, processEnv) {
2812
2827
  LLM_THINKING_LEVEL: parseEnvEnum(e, "PI_RESEARCH_LLM_THINKING_LEVEL", ["off", "minimal", "low", "medium", "high"], DEFAULTS.LLM_THINKING_LEVEL),
2813
2828
  PLANNING_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_PLANNING_MAX_TOKENS", DEFAULTS.PLANNING_MAX_TOKENS, 1024, 131072),
2814
2829
  SYNTHESIS_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_SYNTHESIS_MAX_TOKENS", DEFAULTS.SYNTHESIS_MAX_TOKENS, 1024, 131072),
2830
+ RESEARCHER_MAX_TOKENS: parseEnvNumber(e, "PI_RESEARCH_RESEARCHER_MAX_TOKENS", DEFAULTS.RESEARCHER_MAX_TOKENS, 1024, 131072),
2815
2831
  MIGRATION_STRATEGY: parseEnvEnum(e, "PI_RESEARCH_MIGRATION_STRATEGY", ["drop", "re-embed", "backup"], DEFAULTS.MIGRATION_STRATEGY),
2816
2832
  CONSOLE_LOG: parseEnvBool(e, "PI_RESEARCH_CONSOLE_LOG", DEFAULTS.CONSOLE_LOG),
2817
2833
  RESEARCH_MODEL: parseEnvString(e, "PI_RESEARCH_MODEL", DEFAULTS.RESEARCH_MODEL),
@@ -2999,6 +3015,15 @@ var init_config = __esm({
2999
3015
  * the model's real maxTokens at call time.
3000
3016
  * Not exposed in TUI — controlled via PI_RESEARCH_SYNTHESIS_MAX_TOKENS env var. */
3001
3017
  SYNTHESIS_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 32768 }),
3018
+ /** Max OUTPUT tokens per researcher LLM call (deep and quick sub-agent sessions).
3019
+ * Without a cap the session inherits the model's full catalog ceiling as the
3020
+ * per-request max_tokens — ~230k for large-context models — which providers that
3021
+ * budget-check max_tokens up front (OpenRouter 402s against remaining credits)
3022
+ * reject outright on low balances, even though the actual response would cost
3023
+ * cents. Default 16384: researcher turns are tool calls plus a cited report, far
3024
+ * below this. Clamped to the model's real ceiling at session build time.
3025
+ * Not exposed in TUI — controlled via PI_RESEARCH_RESEARCHER_MAX_TOKENS env var. */
3026
+ RESEARCHER_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 16384 }),
3002
3027
  /** LLM Model override for researcher sub-agents and knowledge synthesis.
3003
3028
  * Format: provider/model-id (e.g. google/gemini-2.0-flash-001) or just model-id.
3004
3029
  * When set, this overrides ctx.model for researcher sub-agents (both deep and quick)
@@ -3076,6 +3101,7 @@ var init_config = __esm({
3076
3101
  "PI_RESEARCH_LLM_THINKING_LEVEL",
3077
3102
  "PI_RESEARCH_PLANNING_MAX_TOKENS",
3078
3103
  "PI_RESEARCH_SYNTHESIS_MAX_TOKENS",
3104
+ "PI_RESEARCH_RESEARCHER_MAX_TOKENS",
3079
3105
  "PI_RESEARCH_MIGRATION_STRATEGY",
3080
3106
  "PI_RESEARCH_CONSOLE_LOG",
3081
3107
  "PI_RESEARCH_MODEL",
@@ -3118,8 +3144,7 @@ function getMaxScrapeBatches(config) {
3118
3144
  }
3119
3145
  }
3120
3146
  function resolveExcludedTools(callerExclusions, configDisabled = []) {
3121
- const base = callerExclusions && callerExclusions.length > 0 ? callerExclusions : DEFAULT_EXCLUDED_TOOLS;
3122
- return [.../* @__PURE__ */ new Set([...base, ...configDisabled])];
3147
+ return [.../* @__PURE__ */ new Set([...DEFAULT_EXCLUDED_TOOLS, ...callerExclusions ?? [], ...configDisabled])];
3123
3148
  }
3124
3149
  var MAX_GATHERING_CALLS, MAX_GREP_CALLS, MAX_SCRAPE_URLS, MAX_TEAM_SIZE_LEVEL_1, MAX_TEAM_SIZE_LEVEL_2, MAX_TEAM_SIZE_LEVEL_3, MAX_ROUNDS_LEVEL_1, MAX_ROUNDS_LEVEL_2, MAX_ROUNDS_LEVEL_3, MAX_EXTRA_ROUNDS_WITH_STEERING, OSV_TIMEOUT_MS, WEBGPU_PROBE_TIMEOUT_MS, MAX_QUERY_LENGTH, MAX_FILENAME_QUERY_LENGTH, MIN_QUERY_LENGTH, MAX_EXPORT_RETRIES, DEFAULT_MAX_RETRIES, DEFAULT_INITIAL_DELAY_MS, DEFAULT_MAX_DELAY_MS, DEFAULT_MODEL_CONTEXT_WINDOW, BATCH_2_DEFAULT_CONCURRENCY, MAX_SCRAPE_CONTENT_CHARS_PER_DOC, RESEARCHER_LAUNCH_DELAY_MS, MAX_QUERIES_PER_RESEARCHER_LEVEL_1, MAX_QUERIES_PER_RESEARCHER_LEVEL_2, MAX_QUERIES_PER_RESEARCHER_LEVEL_3, DEFAULT_EXCLUDED_TOOLS;
3125
3150
  var init_constants = __esm({
@@ -5131,14 +5156,16 @@ function unregisterGlobalEmbedder() {
5131
5156
  async function initializeDawnWebGPU() {
5132
5157
  if (dawnInitialized) return true;
5133
5158
  try {
5134
- const { create, globals } = await import("webgpu");
5135
- Object.assign(globalThis, globals);
5136
- Object.defineProperty(globalThis, "navigator", {
5137
- value: { gpu: create([]) },
5138
- writable: true,
5139
- configurable: true
5159
+ const adapter = await logger.runCapturingStderr(async () => {
5160
+ const { create, globals } = await import("webgpu");
5161
+ Object.assign(globalThis, globals);
5162
+ Object.defineProperty(globalThis, "navigator", {
5163
+ value: { gpu: create([]) },
5164
+ writable: true,
5165
+ configurable: true
5166
+ });
5167
+ return await globalThis.navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
5140
5168
  });
5141
- const adapter = await globalThis.navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
5142
5169
  if (!adapter) {
5143
5170
  logger.warn("[embedder] No WebGPU adapter available \u2014 using CPU.");
5144
5171
  return false;
@@ -16808,21 +16835,21 @@ init_error_tracker();
16808
16835
 
16809
16836
  // src/utils/user-agent.ts
16810
16837
  var USER_AGENTS = [
16811
- // Chrome on Windows (updated 2025)
16812
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
16813
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
16838
+ // Chrome on Windows
16839
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
16840
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
16814
16841
  // Chrome on macOS
16815
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
16816
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
16842
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
16843
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
16817
16844
  // Chrome on Linux
16818
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
16819
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
16820
- // Firefox on Windows (updated 2025)
16821
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
16822
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
16845
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
16846
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
16847
+ // Firefox on Windows
16848
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0",
16849
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
16823
16850
  // Firefox on macOS
16824
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
16825
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
16851
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:153.0) Gecko/20100101 Firefox/153.0",
16852
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0"
16826
16853
  ];
16827
16854
 
16828
16855
  // src/web-research/scraper-types.ts
@@ -17008,17 +17035,30 @@ function ssrfSafeLookup(hostname, options, callback, resolveImpl = dnsCb.lookup)
17008
17035
  return callback(null, safe[0].address, safe[0].family);
17009
17036
  });
17010
17037
  }
17011
- var cachedSsrfDispatcher = void 0;
17012
- async function getSsrfSafeDispatcher() {
17013
- if (cachedSsrfDispatcher !== void 0) return cachedSsrfDispatcher;
17038
+ function isSsrfBlockError(error) {
17039
+ let e = error;
17040
+ for (let depth = 0; depth < 5 && e instanceof Error; depth++) {
17041
+ if (e.code === "ESSRFBLOCKED" || e.message.includes("not allowed")) {
17042
+ return true;
17043
+ }
17044
+ e = e.cause;
17045
+ }
17046
+ return false;
17047
+ }
17048
+ var cachedSsrfFetcher = void 0;
17049
+ async function getSsrfSafeFetcher() {
17050
+ if (cachedSsrfFetcher !== void 0) return cachedSsrfFetcher;
17014
17051
  try {
17015
- const { Agent: Agent2 } = await import("undici");
17016
- cachedSsrfDispatcher = new Agent2({ connect: { lookup: ssrfSafeLookup } });
17052
+ const { Agent: Agent2, fetch: undiciFetch } = await import("undici");
17053
+ cachedSsrfFetcher = {
17054
+ fetch: undiciFetch,
17055
+ dispatcher: new Agent2({ connect: { lookup: ssrfSafeLookup } })
17056
+ };
17017
17057
  } catch (e) {
17018
17058
  logger.warn(`[Scrapers] undici unavailable; connect-time SSRF pinning disabled (request-time validation still active): ${e instanceof Error ? e.message : String(e)}`);
17019
- cachedSsrfDispatcher = null;
17059
+ cachedSsrfFetcher = null;
17020
17060
  }
17021
- return cachedSsrfDispatcher;
17061
+ return cachedSsrfFetcher;
17022
17062
  }
17023
17063
  function formatErrorWithCause(err) {
17024
17064
  try {
@@ -17068,6 +17108,7 @@ function isPrivateIpv6(ip) {
17068
17108
  if (normalized.startsWith("2002:")) return true;
17069
17109
  if (/^2001:0{0,4}:/.test(normalized) || normalized.startsWith("2001::")) return true;
17070
17110
  if (normalized.startsWith("64:ff9b:")) return true;
17111
+ if (/^::(?:\d{1,3}(?:\.\d{1,3}){3}|[0-9a-f]{1,4}(?::[0-9a-f]{1,4})?)$/.test(normalized)) return true;
17071
17112
  if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
17072
17113
  return false;
17073
17114
  }
@@ -17267,7 +17308,7 @@ function decodeHtmlBody(bytes, contentTypeHeader) {
17267
17308
  }
17268
17309
  }
17269
17310
  function trackFetchLayerFailure(url, error) {
17270
- const isSsrf = error instanceof Error && error.message.includes("not allowed");
17311
+ const isSsrf = isSsrfBlockError(error);
17271
17312
  errorTracker.trackError(error instanceof Error ? error : String(error), {
17272
17313
  component: "scrapers",
17273
17314
  operation: "fetch",
@@ -17279,7 +17320,8 @@ function trackFetchLayerFailure(url, error) {
17279
17320
  }
17280
17321
  async function scrapeWithFetch(url, signal) {
17281
17322
  await validateUrlForSSRF(url);
17282
- const dispatcher = await getSsrfSafeDispatcher();
17323
+ const fetcher = await getSsrfSafeFetcher();
17324
+ const doFetch = fetcher?.fetch ?? ((u, init) => fetch(u, init));
17283
17325
  const controller = new AbortController();
17284
17326
  const timeoutId = setTimeout(() => controller.abort(), FETCH_LAYER_TIMEOUT);
17285
17327
  safeUnref(timeoutId);
@@ -17296,16 +17338,17 @@ async function scrapeWithFetch(url, signal) {
17296
17338
  const MAX_REDIRECTS = 10;
17297
17339
  let currentUrl = url;
17298
17340
  let response;
17341
+ const userAgent = getRandomUserAgent();
17299
17342
  for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
17300
- response = await fetch(currentUrl, {
17343
+ response = await doFetch(currentUrl, {
17301
17344
  signal: controller.signal,
17302
17345
  redirect: "manual",
17303
17346
  headers: {
17304
- "User-Agent": getRandomUserAgent(),
17347
+ "User-Agent": userAgent,
17305
17348
  "Accept": "text/html,application/xhtml+xml,application/pdf,*/*;q=0.8"
17306
17349
  },
17307
- // RequestInit's standard type omits undici's `dispatcher`; cast to attach it.
17308
- ...dispatcher ? { dispatcher } : {}
17350
+ // Only ever attached alongside the fetch that honours it (see above).
17351
+ ...fetcher ? { dispatcher: fetcher.dispatcher } : {}
17309
17352
  });
17310
17353
  if (response.status >= 300 && response.status < 400) {
17311
17354
  const location = response.headers.get("location");
@@ -17406,7 +17449,7 @@ async function scrapeWithFetch(url, signal) {
17406
17449
  metrics.observe("scrape_latency_ms", fetchDuration, { layer: "fetch", content_type: "html", status: "success" });
17407
17450
  return { source: "fetch", layer: "fetch", markdown };
17408
17451
  } catch (error) {
17409
- if (error instanceof Error && error.message.includes("not allowed")) {
17452
+ if (isSsrfBlockError(error)) {
17410
17453
  metrics.increment("scrape_operations_total", 1, { layer: "fetch", status: "ssrf_blocked" });
17411
17454
  metrics.observe("scrape_latency_ms", Date.now() - fetchStart, { layer: "fetch", status: "ssrf_blocked" });
17412
17455
  throw error;
@@ -21506,6 +21549,8 @@ async function createResearcherSession(options) {
21506
21549
  cwd
21507
21550
  });
21508
21551
  logger.info(`[Researcher] Using model for researcher ${researcherId}: ${modelToUse.provider}/${modelToUse.id}`);
21552
+ const outputCap = config?.RESEARCHER_MAX_TOKENS ?? 16384;
21553
+ const sessionModel = modelToUse.maxTokens > outputCap ? { ...modelToUse, maxTokens: outputCap } : modelToUse;
21509
21554
  try {
21510
21555
  const tracker = new ToolUsageTracker(createDefaultToolLimits(config));
21511
21556
  const globalLinks = updateGlobalLinks || (() => {
@@ -21546,7 +21591,7 @@ async function createResearcherSession(options) {
21546
21591
  sessionManager: SessionManager.inMemory(),
21547
21592
  // Each researcher gets its own isolated session
21548
21593
  settingsManager: researcherSettings,
21549
- model: modelToUse,
21594
+ model: sessionModel,
21550
21595
  // pi 0.80.8 replaced the SDK's `modelRegistry`/`authStorage` session options
21551
21596
  // with the async `modelRuntime`. Pass the runtime backing OUR registry when
21552
21597
  // this run built one (standalone CLI/SDK): it carries any explicit API key
@@ -21577,7 +21622,7 @@ async function createResearcherSession(options) {
21577
21622
  throw new Error("Session creation returned invalid result");
21578
21623
  }
21579
21624
  sessionRef.session = result.session;
21580
- return { session: result.session, resolvedModel: modelToUse };
21625
+ return { session: result.session, resolvedModel: sessionModel };
21581
21626
  } catch (tailError) {
21582
21627
  const session = result?.session;
21583
21628
  if (session && typeof session.abort === "function") {
@@ -21600,7 +21645,7 @@ async function createResearcherSession(options) {
21600
21645
 
21601
21646
  // src/orchestration/researcher-executor.ts
21602
21647
  init_text_utils();
21603
- init_llm();
21648
+ init_llm_usage();
21604
21649
  init_logger();
21605
21650
  init_metrics();
21606
21651
  init_service_interfaces();
@@ -22036,12 +22081,13 @@ ${prompt}`);
22036
22081
  if (msg2?.["role"] !== "assistant") return;
22037
22082
  const rawUsage = msg2["usage"];
22038
22083
  if (rawUsage) {
22039
- const { tokens, cost } = extractUsage(resolvedModel, rawUsage);
22084
+ const { tokens, cost } = recordLlmUsage(resolvedModel, rawUsage, {
22085
+ component: "researcher",
22086
+ complexity,
22087
+ observer: { onTokensConsumed: (t, c) => safeObserve(() => observer?.onTokensConsumed?.(t, c)) }
22088
+ });
22040
22089
  if (tokens > 0 || cost > 0) {
22041
- metrics.increment("llm_tokens_total", tokens, { component: "researcher", complexity: String(complexity) });
22042
- metrics.increment("llm_cost_total", cost, { component: "researcher", complexity: String(complexity) });
22043
22090
  safeObserve(() => observer?.onResearcherProgress?.(id, void 0, tokens, cost));
22044
- safeObserve(() => observer?.onTokensConsumed?.(tokens, cost));
22045
22091
  }
22046
22092
  }
22047
22093
  } else if (event.type === "tool_execution_start") {
@@ -22168,7 +22214,7 @@ ${responseText}`);
22168
22214
 
22169
22215
  // src/orchestration/quick-research-orchestrator.ts
22170
22216
  init_prompts();
22171
- init_llm();
22217
+ init_llm_usage();
22172
22218
  init_logger();
22173
22219
  init_config();
22174
22220
  init_text_utils();
@@ -22449,12 +22495,13 @@ ${prompt}`);
22449
22495
  if (msg2?.["role"] !== "assistant") return;
22450
22496
  const rawUsage = msg2["usage"];
22451
22497
  if (rawUsage) {
22452
- const { tokens, cost } = extractUsage(resolvedModel, rawUsage);
22498
+ const { tokens, cost } = recordLlmUsage(resolvedModel, rawUsage, {
22499
+ component: "quick_researcher",
22500
+ complexity: 0,
22501
+ observer
22502
+ });
22453
22503
  if (tokens > 0 || cost > 0) {
22454
- metrics.increment("llm_tokens_total", tokens, { component: "quick_researcher", complexity: "0" });
22455
- metrics.increment("llm_cost_total", cost, { component: "quick_researcher", complexity: "0" });
22456
22504
  observer?.onResearcherProgress?.("quick", void 0, tokens, cost);
22457
- observer?.onTokensConsumed?.(tokens, cost);
22458
22505
  }
22459
22506
  }
22460
22507
  } else if (event.type === "tool_execution_start") {
@@ -25674,7 +25721,7 @@ function parseArgs(argv) {
25674
25721
  }
25675
25722
  if (a === "--depth") {
25676
25723
  const v = rest[++i];
25677
- const n = v === void 0 ? NaN : parseInt(v, 10);
25724
+ const n = v !== void 0 && /^\d+$/.test(v.trim()) ? Number(v.trim()) : NaN;
25678
25725
  if (isNaN(n) || n < 0 || n > 3) throw new UsageError("--depth must be an integer 0\u20133.");
25679
25726
  depth = n;
25680
25727
  } else if (a === "--model") {
@@ -77,22 +77,36 @@ function redactSecrets(message) {
77
77
  function stripTerminalEscapes(message) {
78
78
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
79
79
  }
80
- var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
80
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, B_LEFT, B_RIGHT, COOKIE_HEADER_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN, CONTROL_CHARS_PATTERN, REDACT_SCAN_LENGTH, TOKEN_CHAR_PATTERN, REDACT_SCAN_EXTENSION_MAX, TERMINAL_ESCAPE_PATTERN;
81
81
  var init_log_utils = __esm({
82
82
  "src/utils/log-utils.ts"() {
83
83
  "use strict";
84
84
  logContextStorage = new AsyncLocalStorage();
85
85
  MAX_LOG_MESSAGE_LENGTH = 1e4;
86
86
  ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
87
- URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
88
- COOKIE_HEADER_PATTERN = /\b(set[_-]?cookie|cookie)\b(["']?\s*[:=]\s*["']?)([^\r\n]+)/gi;
89
- SENSITIVE_KV_PATTERN = /\b(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)\b(["']?\s*[:=]\s*["']?)([^\s"',&)]+)/gi;
90
- KNOWN_TOKEN_PATTERN = /\b(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})\b/g;
91
- JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
92
- BASIC_AUTH_PATTERN = /\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}/gi;
93
- BEARER_AUTH_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
94
- PROVIDER_TOKEN_PATTERN = /\b(AIza[0-9A-Za-z_-]{35}|ya29\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})\b/g;
95
- LONG_HEX_SECRET_PATTERN = /\b[0-9a-fA-F]{32,}\b/g;
87
+ URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]{0,31}:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
88
+ B_LEFT = "(?<![A-Za-z0-9])";
89
+ B_RIGHT = "(?![A-Za-z0-9])";
90
+ COOKIE_HEADER_PATTERN = new RegExp(
91
+ `${B_LEFT}(set[_-]?cookie|cookie)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\r\\n]+)`,
92
+ "gi"
93
+ );
94
+ SENSITIVE_KV_PATTERN = new RegExp(
95
+ `${B_LEFT}(api[_-]?key|apikey|access[_-]?token|auth[_-]?token|refresh[_-]?token|secret|client[_-]?secret|password|passwd|pwd|authorization|bearer|set[_-]?cookie|cookie|session[_-]?id|session|csrf[_-]?token|xsrf[_-]?token|private[_-]?key|key)${B_RIGHT}(["']?\\s*[:=]\\s*["']?)([^\\s"',&)]+)`,
96
+ "gi"
97
+ );
98
+ KNOWN_TOKEN_PATTERN = new RegExp(
99
+ `${B_LEFT}(sk-[A-Za-z0-9_-]{16,}|gh[posru]_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|AKIA[0-9A-Z]{16}|xox[baprs]-[A-Za-z0-9-]{10,})${B_RIGHT}`,
100
+ "g"
101
+ );
102
+ JWT_PATTERN = new RegExp(`${B_LEFT}eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+`, "g");
103
+ BASIC_AUTH_PATTERN = new RegExp(`${B_LEFT}Basic\\s+[A-Za-z0-9+/]{16,}={0,2}`, "gi");
104
+ BEARER_AUTH_PATTERN = new RegExp(`${B_LEFT}Bearer\\s+[A-Za-z0-9._~+/-]+=*`, "gi");
105
+ PROVIDER_TOKEN_PATTERN = new RegExp(
106
+ `${B_LEFT}(AIza[0-9A-Za-z_-]{35}|ya29\\.[0-9A-Za-z_-]{20,}|[sp]k_(?:live|test)_[0-9A-Za-z]{16,})${B_RIGHT}`,
107
+ "g"
108
+ );
109
+ LONG_HEX_SECRET_PATTERN = new RegExp(`${B_LEFT}[0-9a-fA-F]{32,}${B_RIGHT}`, "g");
96
110
  CONTROL_CHARS_PATTERN = /[\x00-\x1f\x7f]/g;
97
111
  REDACT_SCAN_LENGTH = MAX_LOG_MESSAGE_LENGTH * 4;
98
112
  TOKEN_CHAR_PATTERN = /[A-Za-z0-9_\-./+=]/;
@@ -110,21 +124,21 @@ var init_user_agent = __esm({
110
124
  "src/utils/user-agent.ts"() {
111
125
  "use strict";
112
126
  USER_AGENTS = [
113
- // Chrome on Windows (updated 2025)
114
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
115
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
127
+ // Chrome on Windows
128
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
129
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
116
130
  // Chrome on macOS
117
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
118
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
131
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
132
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
119
133
  // Chrome on Linux
120
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
121
- "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
122
- // Firefox on Windows (updated 2025)
123
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
124
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
134
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36",
135
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36",
136
+ // Firefox on Windows
137
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0",
138
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:152.0) Gecko/20100101 Firefox/152.0",
125
139
  // Firefox on macOS
126
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
127
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
140
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:153.0) Gecko/20100101 Firefox/153.0",
141
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:152.0) Gecko/20100101 Firefox/152.0"
128
142
  ];
129
143
  }
130
144
  });
@@ -1498,8 +1512,9 @@ __export(scraper_utils_exports, {
1498
1512
  extractDomain: () => extractDomain,
1499
1513
  formatErrorWithCause: () => formatErrorWithCause,
1500
1514
  getRandomUserAgent: () => getRandomUserAgent,
1501
- getSsrfSafeDispatcher: () => getSsrfSafeDispatcher,
1515
+ getSsrfSafeFetcher: () => getSsrfSafeFetcher,
1502
1516
  isBenignScrapeFailure: () => isBenignScrapeFailure,
1517
+ isSsrfBlockError: () => isSsrfBlockError,
1503
1518
  ssrfSafeLookup: () => ssrfSafeLookup,
1504
1519
  stripImageLinks: () => stripImageLinks,
1505
1520
  validateContent: () => validateContent,
@@ -1619,16 +1634,29 @@ function ssrfSafeLookup(hostname, options, callback, resolveImpl = dnsCb.lookup)
1619
1634
  return callback(null, safe[0].address, safe[0].family);
1620
1635
  });
1621
1636
  }
1622
- async function getSsrfSafeDispatcher() {
1623
- if (cachedSsrfDispatcher !== void 0) return cachedSsrfDispatcher;
1637
+ function isSsrfBlockError(error) {
1638
+ let e = error;
1639
+ for (let depth = 0; depth < 5 && e instanceof Error; depth++) {
1640
+ if (e.code === "ESSRFBLOCKED" || e.message.includes("not allowed")) {
1641
+ return true;
1642
+ }
1643
+ e = e.cause;
1644
+ }
1645
+ return false;
1646
+ }
1647
+ async function getSsrfSafeFetcher() {
1648
+ if (cachedSsrfFetcher !== void 0) return cachedSsrfFetcher;
1624
1649
  try {
1625
- const { Agent } = await import("undici");
1626
- cachedSsrfDispatcher = new Agent({ connect: { lookup: ssrfSafeLookup } });
1650
+ const { Agent, fetch: undiciFetch } = await import("undici");
1651
+ cachedSsrfFetcher = {
1652
+ fetch: undiciFetch,
1653
+ dispatcher: new Agent({ connect: { lookup: ssrfSafeLookup } })
1654
+ };
1627
1655
  } catch (e) {
1628
1656
  logger.warn(`[Scrapers] undici unavailable; connect-time SSRF pinning disabled (request-time validation still active): ${e instanceof Error ? e.message : String(e)}`);
1629
- cachedSsrfDispatcher = null;
1657
+ cachedSsrfFetcher = null;
1630
1658
  }
1631
- return cachedSsrfDispatcher;
1659
+ return cachedSsrfFetcher;
1632
1660
  }
1633
1661
  function formatErrorWithCause(err) {
1634
1662
  try {
@@ -1678,6 +1706,7 @@ function isPrivateIpv6(ip) {
1678
1706
  if (normalized.startsWith("2002:")) return true;
1679
1707
  if (/^2001:0{0,4}:/.test(normalized) || normalized.startsWith("2001::")) return true;
1680
1708
  if (normalized.startsWith("64:ff9b:")) return true;
1709
+ if (/^::(?:\d{1,3}(?:\.\d{1,3}){3}|[0-9a-f]{1,4}(?::[0-9a-f]{1,4})?)$/.test(normalized)) return true;
1681
1710
  if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
1682
1711
  return false;
1683
1712
  }
@@ -1761,7 +1790,7 @@ function validateContent(html, markdown, url) {
1761
1790
  throw error;
1762
1791
  }
1763
1792
  }
1764
- var cachedSsrfDispatcher;
1793
+ var cachedSsrfFetcher;
1765
1794
  var init_scraper_utils = __esm({
1766
1795
  "src/web-research/scraper-utils.ts"() {
1767
1796
  "use strict";
@@ -1770,7 +1799,7 @@ var init_scraper_utils = __esm({
1770
1799
  init_logger();
1771
1800
  init_scraper_types();
1772
1801
  init_browser_error_utils();
1773
- cachedSsrfDispatcher = void 0;
1802
+ cachedSsrfFetcher = void 0;
1774
1803
  }
1775
1804
  });
1776
1805
 
@@ -2515,6 +2544,15 @@ var ConfigSchema = Type.Object({
2515
2544
  * the model's real maxTokens at call time.
2516
2545
  * Not exposed in TUI — controlled via PI_RESEARCH_SYNTHESIS_MAX_TOKENS env var. */
2517
2546
  SYNTHESIS_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 32768 }),
2547
+ /** Max OUTPUT tokens per researcher LLM call (deep and quick sub-agent sessions).
2548
+ * Without a cap the session inherits the model's full catalog ceiling as the
2549
+ * per-request max_tokens — ~230k for large-context models — which providers that
2550
+ * budget-check max_tokens up front (OpenRouter 402s against remaining credits)
2551
+ * reject outright on low balances, even though the actual response would cost
2552
+ * cents. Default 16384: researcher turns are tool calls plus a cited report, far
2553
+ * below this. Clamped to the model's real ceiling at session build time.
2554
+ * Not exposed in TUI — controlled via PI_RESEARCH_RESEARCHER_MAX_TOKENS env var. */
2555
+ RESEARCHER_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 16384 }),
2518
2556
  /** LLM Model override for researcher sub-agents and knowledge synthesis.
2519
2557
  * Format: provider/model-id (e.g. google/gemini-2.0-flash-001) or just model-id.
2520
2558
  * When set, this overrides ctx.model for researcher sub-agents (both deep and quick)
@@ -115,12 +115,14 @@ agent to invoke:
115
115
  In deep research `search` is excluded — the coordinator runs the search burst and hands
116
116
  out URLs directly.
117
117
 
118
- `grep` is excluded by default at **every** depth and on every front-end (CLI, SDK, agent
119
- skill, pi extension): this is web research, and a capable model otherwise spends turns
120
- searching the local filesystem. Passing an explicit `excludeTools` list — `--exclude-tools`
121
- on the CLI, the `excludeTools` tool parameter in the extension — REPLACES that default, so
122
- naming other tools re-enables `grep`. `PI_RESEARCH_DISABLED_TOOLS` is separate and strictly
123
- additive: it can only ever remove capabilities (see [CONFIGURATION.md](CONFIGURATION.md)).
118
+ `grep` is excluded at **every** depth and on every front-end (CLI, SDK, agent skill,
119
+ pi extension): this is web research, and a capable model otherwise spends turns
120
+ searching the local filesystem. Both exclusion surfaces — the `excludeTools` list
121
+ (`--exclude-tools` on the CLI, the `excludeTools` tool parameter in the extension) and
122
+ `PI_RESEARCH_DISABLED_TOOLS` — are strictly additive on top of that default: they can
123
+ only ever remove capabilities (see [CONFIGURATION.md](CONFIGURATION.md)). Before 1.3.10
124
+ a non-empty `excludeTools` list replaced the default, so naming any other tool silently
125
+ re-enabled `grep`.
124
126
 
125
127
  Researchers cannot write files, run shell commands, or reach the network outside these
126
128
  tools.
@@ -318,7 +320,7 @@ other). This sits above `@lancedb/lancedb` 0.29's declared Arrow peer ceiling
318
320
  whenever `@lancedb/lancedb` is upgraded.
319
321
 
320
322
  Pinned validation library — `typebox` is pinned to the exact version the pi host packages
321
- depend on (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent` 0.84.1 pin `1.3.7`). Every
323
+ depend on (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent` 0.84.2 pin `1.3.7`). Every
322
324
  tool's parameter schema is built with TypeBox here and handed across the boundary to pi's
323
325
  tool system, so the two must agree on `Value.Check`/`Convert` semantics. A floating `^1.1.38`
324
326
  range let a fresh consumer install resolve pi-research to a newer TypeBox than pi's, shipping