@lincoln504/pi-research 1.3.10 → 1.3.12

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.
Files changed (38) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +264 -61
  3. package/dist/prompts/system-coordinator.md +1 -1
  4. package/dist/prompts/system-lead-evaluator.md +1 -1
  5. package/dist/thread-worker.mjs +31 -21
  6. package/docs/ARCHITECTURE.md +60 -22
  7. package/docs/CHANGELOG.md +103 -1
  8. package/docs/CONFIGURATION.md +6 -5
  9. package/package.json +18 -10
  10. package/scripts/cleanup.cjs +26 -5
  11. package/scripts/setup.cjs +25 -1
  12. package/skills/pi-research/SKILL.md +1 -1
  13. package/src/cli.ts +94 -13
  14. package/src/config.ts +6 -15
  15. package/src/constants.ts +13 -0
  16. package/src/core/service-registry.ts +19 -3
  17. package/src/infrastructure/browser/config.ts +36 -5
  18. package/src/infrastructure/browser/task-execution-service.ts +12 -2
  19. package/src/infrastructure/browser/thread-worker-browser.ts +34 -18
  20. package/src/infrastructure/browser/thread-worker.mjs +31 -21
  21. package/src/infrastructure/gpu-resource-service.ts +15 -1
  22. package/src/knowledge/embedder-init.ts +34 -2
  23. package/src/knowledge/store.ts +75 -6
  24. package/src/knowledge/writer-queue.ts +28 -0
  25. package/src/orchestration/research-orchestration-service.ts +35 -1
  26. package/src/orchestration/research-synthesis-service.ts +4 -2
  27. package/src/orchestration/researcher.ts +2 -13
  28. package/src/prompts/system-coordinator.md +1 -1
  29. package/src/prompts/system-lead-evaluator.md +1 -1
  30. package/src/sdk.ts +10 -1
  31. package/src/security/nvd-client.ts +55 -4
  32. package/src/security/osv-client.ts +13 -1
  33. package/src/tools/research-knowledge-search.ts +18 -1
  34. package/src/tools/research-tool-definition.ts +8 -1
  35. package/src/utils/citation-utils.ts +4 -2
  36. package/src/utils/log-utils.ts +16 -1
  37. package/src/utils/text-utils.ts +17 -3
  38. package/src/web-research/scraper-utils.ts +55 -14
@@ -277,10 +277,18 @@ export interface SsrfSafeFetcher {
277
277
  dispatcher: unknown;
278
278
  }
279
279
 
280
- // Cached after first construction. `undefined` = not yet attempted, `null` =
281
- // undici unavailable (fall back to the global fetch with request-time
282
- // validation only).
283
- let cachedSsrfFetcher: SsrfSafeFetcher | null | undefined = undefined;
280
+ // Cached after first construction. `undefined` = not yet attempted; the promise
281
+ // resolves to `null` when undici is unavailable (fall back to the global fetch
282
+ // with request-time validation only).
283
+ //
284
+ // The PROMISE is cached, not the resolved value. Caching the value left a window
285
+ // between the `undefined` check and the assignment — an `await import('undici')`
286
+ // wide — during which every concurrent first caller built its OWN Agent. Only the
287
+ // last assignment survived in the cache, so the others were handed live Agents
288
+ // that nothing tracked: they opened keep-alive sockets and no shutdown path could
289
+ // ever close them, which is precisely the leak disposeSsrfSafeFetcher exists to
290
+ // prevent. Scrapes run concurrently by design, so the very first burst hit this.
291
+ let cachedSsrfFetcher: Promise<SsrfSafeFetcher | null> | undefined = undefined;
284
292
 
285
293
  /**
286
294
  * Build (once) an undici Agent whose connector resolves via ssrfSafeLookup, so
@@ -304,19 +312,52 @@ let cachedSsrfFetcher: SsrfSafeFetcher | null | undefined = undefined;
304
312
  * Returns null if undici can't be loaded; callers then use the global fetch with
305
313
  * no dispatcher (request-time validation still applies).
306
314
  */
307
- export async function getSsrfSafeFetcher(): Promise<SsrfSafeFetcher | null> {
308
- if (cachedSsrfFetcher !== undefined) return cachedSsrfFetcher;
315
+ export function getSsrfSafeFetcher(): Promise<SsrfSafeFetcher | null> {
316
+ // Assigned before the first await, so concurrent callers all join this one
317
+ // construction rather than each starting their own.
318
+ cachedSsrfFetcher ??= (async () => {
319
+ try {
320
+ const { Agent, fetch: undiciFetch } = await import('undici');
321
+ return {
322
+ fetch: undiciFetch as unknown as SsrfSafeFetcher['fetch'],
323
+ dispatcher: new Agent({ connect: { lookup: ssrfSafeLookup } }),
324
+ };
325
+ } catch (e) {
326
+ logger.warn(`[Scrapers] undici unavailable; connect-time SSRF pinning disabled (request-time validation still active): ${e instanceof Error ? e.message : String(e)}`);
327
+ return null;
328
+ }
329
+ })();
330
+ return cachedSsrfFetcher;
331
+ }
332
+
333
+ /**
334
+ * Close the cached SSRF Agent and drop the cache.
335
+ *
336
+ * The Agent is a process-lifetime singleton holding keep-alive sockets to every
337
+ * host scraped so far. The CLI hard-exits so it never notices, but an embedding
338
+ * host (the pi extension) keeps the process alive across SDK
339
+ * init/shutdown cycles, where those sockets would linger past a teardown that
340
+ * claims to have released everything.
341
+ *
342
+ * Clearing the cache is not optional: the SDK can be re-initialized in the same
343
+ * process, and a closed Agent left in the cache would fail every subsequent
344
+ * scrape. Resetting to `undefined` (not `null`) makes the next call rebuild it.
345
+ */
346
+ export async function disposeSsrfSafeFetcher(): Promise<void> {
347
+ const pending = cachedSsrfFetcher;
348
+ cachedSsrfFetcher = undefined;
349
+ if (pending === undefined) return;
350
+ // Await the in-flight construction rather than skipping it: a dispose racing
351
+ // the first getSsrfSafeFetcher() would otherwise leave the Agent it is still
352
+ // building unreferenced and unclosed.
353
+ const cached = await pending.catch(() => null);
354
+ const closable = cached?.dispatcher as { close?: () => Promise<void> } | undefined;
355
+ if (!closable?.close) return;
309
356
  try {
310
- const { Agent, fetch: undiciFetch } = await import('undici');
311
- cachedSsrfFetcher = {
312
- fetch: undiciFetch as unknown as SsrfSafeFetcher['fetch'],
313
- dispatcher: new Agent({ connect: { lookup: ssrfSafeLookup } }),
314
- };
357
+ await closable.close();
315
358
  } catch (e) {
316
- logger.warn(`[Scrapers] undici unavailable; connect-time SSRF pinning disabled (request-time validation still active): ${e instanceof Error ? e.message : String(e)}`);
317
- cachedSsrfFetcher = null;
359
+ logger.debug(`[Scrapers] Error closing SSRF dispatcher: ${e instanceof Error ? e.message : String(e)}`);
318
360
  }
319
- return cachedSsrfFetcher;
320
361
  }
321
362
 
322
363
  /**