@lincoln504/pi-research 1.4.1 → 1.4.3

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 (34) hide show
  1. package/dist/cli.mjs +271 -156
  2. package/dist/thread-worker.mjs +91 -59
  3. package/docs/CHANGELOG.md +76 -0
  4. package/docs/CONFIGURATION.md +1 -1
  5. package/package.json +1 -1
  6. package/skills/pi-research/SKILL.md +1 -1
  7. package/src/config.ts +10 -4
  8. package/src/core/planning-service.ts +55 -8
  9. package/src/infrastructure/browser/browser-task-scheduler.ts +16 -9
  10. package/src/infrastructure/browser/config.ts +40 -2
  11. package/src/infrastructure/browser/thread-worker-browser.ts +69 -48
  12. package/src/infrastructure/browser/thread-worker-messaging.ts +61 -4
  13. package/src/infrastructure/browser/thread-worker.mjs +91 -59
  14. package/src/infrastructure/browser/thread-worker.ts +13 -5
  15. package/src/infrastructure/embedding/embedding-server.ts +34 -0
  16. package/src/knowledge/embedder-init.ts +3 -2
  17. package/src/knowledge/embedder.ts +11 -3
  18. package/src/knowledge/index.ts +36 -10
  19. package/src/knowledge/store.ts +29 -5
  20. package/src/orchestration/deep-research-orchestrator.ts +3 -1
  21. package/src/orchestration/session-state.ts +16 -0
  22. package/src/sdk.ts +10 -0
  23. package/src/skill-install/skill-installer.ts +5 -1
  24. package/src/stackexchange/index.ts +3 -0
  25. package/src/stackexchange/rest-client.ts +7 -12
  26. package/src/tools/research-tool-definition.ts +38 -7
  27. package/src/tools/security.ts +13 -10
  28. package/src/utils/coverage-digest.ts +1 -1
  29. package/src/utils/host-config.ts +12 -1
  30. package/src/utils/log-utils.ts +10 -0
  31. package/src/web-research/browser-search.ts +11 -4
  32. package/src/web-research/web-scraper.ts +27 -16
  33. package/src/youtube/potoken.ts +8 -1
  34. package/src/youtube/transcript-client.ts +56 -5
package/dist/cli.mjs CHANGED
@@ -241,6 +241,8 @@ function redactSecrets(message) {
241
241
  }
242
242
  if (out.length > MAX_LOG_MESSAGE_LENGTH) {
243
243
  out = `${out.slice(0, keep)}\u2026[truncated ${Math.max(rawLength, out.length) - keep} chars]`;
244
+ } else if (rawLength > scanEnd) {
245
+ out = `${out}\u2026[truncated ${rawLength - scanEnd} chars]`;
244
246
  }
245
247
  return out;
246
248
  }
@@ -2279,7 +2281,9 @@ import { createRequire as createRequire2 } from "node:module";
2279
2281
  function getConfigDirName() {
2280
2282
  if (_configDirName !== void 0) return _configDirName;
2281
2283
  const override = process.env["PI_RESEARCH_CONFIG_DIR_NAME"];
2282
- if (override) return _configDirName = override;
2284
+ if (override && !override.includes("/") && !override.includes("\\") && override !== "." && override !== "..") {
2285
+ return _configDirName = override;
2286
+ }
2283
2287
  try {
2284
2288
  const require2 = createRequire2(import.meta.url);
2285
2289
  const host = require2("@earendil-works/pi-coding-agent");
@@ -3031,10 +3035,16 @@ var init_config = __esm({
3031
3035
  SEARCH_TIMEOUT_MS: Type.Number({ minimum: 5e3, maximum: 12e4, default: 45e3 }),
3032
3036
  /** TUI refresh debounce in milliseconds (default: 100ms) */
3033
3037
  TUI_REFRESH_DEBOUNCE_MS: Type.Number({ minimum: 0, maximum: 1e3, default: 100 }),
3034
- /** Queue-wait / overhead margin (ms) added on top of each browser operation's own nav
3035
- * timeout to form its scheduler task-timeout ceiling: a search task times out after
3036
- * SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this. Sized so a task is
3037
- * never killed before it has used its full nav budget plus time waiting in the queue.
3038
+ /** Overhead margin (ms) added on top of each browser operation's own nav timeout to
3039
+ * form its scheduler task-timeout ceiling: a search task times out after
3040
+ * SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this — covering
3041
+ * per-task work beyond raw navigation (page setup, result parsing). Queue wait is
3042
+ * NOT part of this margin: the deadline is armed on dispatch, not on enqueue, so
3043
+ * time spent waiting for a free worker never counts against it (see
3044
+ * browser-task-scheduler.ts). Cold browser-launch/context-creation time is also
3045
+ * separate from this margin — COLD_START_ALLOWANCE_MS
3046
+ * (infrastructure/browser/config.ts) covers that unconditionally on top of it,
3047
+ * since it isn't something a user should need to tune.
3038
3048
  * (default: 10000) */
3039
3049
  BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
3040
3050
  /** Timeout for coordinator/research-lead/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
@@ -4330,12 +4340,12 @@ async function isModelCached(model) {
4330
4340
  const cacheDir = env.cacheDir;
4331
4341
  if (!cacheDir) return false;
4332
4342
  const { access: access3, readdir: readdir6, readFile: readFile7, stat: stat6, rm: rm3 } = await import("node:fs/promises");
4333
- const path19 = await import("node:path");
4334
- const modelDir = path19.default.join(cacheDir, model);
4335
- const onnxDir = path19.default.join(modelDir, "onnx");
4336
- await access3(path19.default.join(onnxDir, "model.onnx"));
4343
+ const path20 = await import("node:path");
4344
+ const modelDir = path20.default.join(cacheDir, model);
4345
+ const onnxDir = path20.default.join(modelDir, "onnx");
4346
+ await access3(path20.default.join(onnxDir, "model.onnx"));
4337
4347
  try {
4338
- const cfgRaw = await readFile7(path19.default.join(modelDir, "config.json"), "utf-8");
4348
+ const cfgRaw = await readFile7(path20.default.join(modelDir, "config.json"), "utf-8");
4339
4349
  JSON.parse(cfgRaw);
4340
4350
  } catch {
4341
4351
  return false;
@@ -4343,7 +4353,7 @@ async function isModelCached(model) {
4343
4353
  const tokenizerFiles = ["tokenizer.json", "tokenizer.model", "vocab.txt"];
4344
4354
  let hasTokenizer = false;
4345
4355
  for (const f of tokenizerFiles) {
4346
- const st = await stat6(path19.default.join(modelDir, f)).catch(() => null);
4356
+ const st = await stat6(path20.default.join(modelDir, f)).catch(() => null);
4347
4357
  if (st && st.size > 0) {
4348
4358
  hasTokenizer = true;
4349
4359
  break;
@@ -4353,7 +4363,7 @@ async function isModelCached(model) {
4353
4363
  const entries = await readdir6(onnxDir).catch(() => []);
4354
4364
  const hasExternalWeights = entries.includes("model.onnx_data");
4355
4365
  if (hasExternalWeights) {
4356
- const dataStat = await stat6(path19.default.join(onnxDir, "model.onnx_data")).catch(() => null);
4366
+ const dataStat = await stat6(path20.default.join(onnxDir, "model.onnx_data")).catch(() => null);
4357
4367
  if (!dataStat || dataStat.size === 0) return false;
4358
4368
  }
4359
4369
  const partialArtifacts = entries.filter(
@@ -4362,7 +4372,7 @@ async function isModelCached(model) {
4362
4372
  if (partialArtifacts.length > 0) {
4363
4373
  if (hasExternalWeights) {
4364
4374
  for (const f of partialArtifacts) {
4365
- await rm3(path19.default.join(onnxDir, f), { force: true }).catch(() => {
4375
+ await rm3(path20.default.join(onnxDir, f), { force: true }).catch(() => {
4366
4376
  });
4367
4377
  }
4368
4378
  } else {
@@ -4384,10 +4394,10 @@ async function purgeModelCache(model) {
4384
4394
  const cacheDir = env.cacheDir;
4385
4395
  if (!cacheDir) return;
4386
4396
  const { rm: rm3 } = await import("node:fs/promises");
4387
- const path19 = await import("node:path");
4388
- const resolvedCache = path19.default.resolve(cacheDir);
4389
- const modelDir = path19.default.resolve(resolvedCache, model);
4390
- if (!model || path19.default.isAbsolute(model) || modelDir === resolvedCache || !modelDir.startsWith(resolvedCache + path19.default.sep)) {
4397
+ const path20 = await import("node:path");
4398
+ const resolvedCache = path20.default.resolve(cacheDir);
4399
+ const modelDir = path20.default.resolve(resolvedCache, model);
4400
+ if (!model || path20.default.isAbsolute(model) || modelDir === resolvedCache || !modelDir.startsWith(resolvedCache + path20.default.sep)) {
4391
4401
  logger.warn(`[embedder] Refusing to purge unsafe model-cache path for model id ${JSON.stringify(model)}`);
4392
4402
  return;
4393
4403
  }
@@ -4470,7 +4480,7 @@ async function handleWebGPULoadError(loadErr, pipeline, stateManager, gpuLockHel
4470
4480
  return { success: false, error };
4471
4481
  }
4472
4482
  }
4473
- async function handleWebGPUWarmupError(warmupErr, pipeline, stateManager, gpuLockHeld, model, initializationTimeoutMs, useCache) {
4483
+ async function handleWebGPUWarmupError(warmupErr, pipeline, stateManager, gpuLockHeld, model, initializationTimeoutMs, useCache, poolingMode = "mean") {
4474
4484
  markWebGpuFallback();
4475
4485
  logger.warn(
4476
4486
  "[embedder] WebGPU error during warmup \u2014 falling back to CPU. The underlying cause (commonly VRAM exhaustion) is in the native Dawn/ONNX output logged just above this line."
@@ -4487,7 +4497,7 @@ async function handleWebGPUWarmupError(warmupErr, pipeline, stateManager, gpuLoc
4487
4497
  try {
4488
4498
  const cpuPipeline = await loadModelOnCPU(model, initializationTimeoutMs, useCache);
4489
4499
  logger.info("[embedder] CPU pipeline loaded successfully");
4490
- const { dummy, success, error: cpuWarmupErr } = await warmupPipeline(cpuPipeline, "mean", useCache);
4500
+ const { dummy, success, error: cpuWarmupErr } = await warmupPipeline(cpuPipeline, poolingMode, useCache);
4491
4501
  if (!success || cpuWarmupErr) {
4492
4502
  logger.error("[embedder] CPU fallback pipeline warmup failed:", cpuWarmupErr);
4493
4503
  if (cpuPipeline) {
@@ -4906,7 +4916,8 @@ var init_embedder = __esm({
4906
4916
  this.gpuLockHeld,
4907
4917
  this.model,
4908
4918
  this.initializationTimeoutMs,
4909
- this.useCache
4919
+ this.useCache,
4920
+ this.poolingMode
4910
4921
  );
4911
4922
  if (!result.success) {
4912
4923
  throw result.error;
@@ -4968,9 +4979,12 @@ var init_embedder = __esm({
4968
4979
  truncateText(text) {
4969
4980
  const maxChars = this.maxTokens * this.charsPerToken;
4970
4981
  if (text.length > maxChars) {
4982
+ let keep = maxChars;
4983
+ const lastKeptUnit = text.charCodeAt(keep - 1);
4984
+ if (lastKeptUnit >= 55296 && lastKeptUnit <= 56319) keep -= 1;
4971
4985
  metrics.increment("embedder_truncations_total", 1, { model: this.model });
4972
- logger.debug(`[embedder] Truncated input for embedding: ${text.length} -> ${maxChars} chars (model=${this.model})`);
4973
- return text.slice(0, maxChars);
4986
+ logger.debug(`[embedder] Truncated input for embedding: ${text.length} -> ${keep} chars (model=${this.model})`);
4987
+ return text.slice(0, keep);
4974
4988
  }
4975
4989
  return text;
4976
4990
  }
@@ -5432,6 +5446,11 @@ var init_embedding_server = __esm({
5432
5446
  hardDeadlineMs;
5433
5447
  onPoison;
5434
5448
  poisoned = null;
5449
+ // The currently-executing task's real work promise (not the soft-timeout race) —
5450
+ // set only while a native call is actually in flight. Lets a caller that needs
5451
+ // to dispose the underlying pipeline (EmbeddingServer.shutdown) wait for true
5452
+ // idleness instead of racing disposal against a still-running inference.
5453
+ currentWork = null;
5435
5454
  constructor(maxDepth = 200, timeoutMs = 12e4, hardDeadlineMs = 6e5, onPoison) {
5436
5455
  this.maxDepth = maxDepth;
5437
5456
  this.timeoutMs = timeoutMs;
@@ -5457,6 +5476,7 @@ var init_embedding_server = __esm({
5457
5476
  let hardTimer;
5458
5477
  try {
5459
5478
  work = fn();
5479
+ this.currentWork = work;
5460
5480
  hardTimer = setTimeout(() => {
5461
5481
  if (settled) return;
5462
5482
  this.poison(new Error(
@@ -5477,6 +5497,7 @@ var init_embedding_server = __esm({
5477
5497
  } finally {
5478
5498
  settled = true;
5479
5499
  if (hardTimer !== void 0) clearTimeout(hardTimer);
5500
+ if (this.currentWork === work) this.currentWork = null;
5480
5501
  }
5481
5502
  }
5482
5503
  };
@@ -5484,6 +5505,23 @@ var init_embedding_server = __esm({
5484
5505
  if (!this.running) void this.pump();
5485
5506
  });
5486
5507
  }
5508
+ /**
5509
+ * Resolve once no native call is in flight. Used by EmbeddingServer.shutdown()
5510
+ * to wait out a still-running inference before disposing the pipeline —
5511
+ * without this, disposal could run concurrently with a legitimately slow (not
5512
+ * hung) embed/embedMany call on the same re-entrant-locked GPU context, the
5513
+ * exact native-segfault class this queue exists to prevent. No timeout: a
5514
+ * promise that truly never settles is already an accepted, pre-existing
5515
+ * leak-to-process-exit scenario elsewhere in this class (see the poison path's
5516
+ * disposeEmbedder=false handling) — better to leak than risk concurrent
5517
+ * native access.
5518
+ */
5519
+ async waitForIdle() {
5520
+ while (this.currentWork) {
5521
+ await this.currentWork.catch(() => {
5522
+ });
5523
+ }
5524
+ }
5487
5525
  poison(err) {
5488
5526
  if (this.poisoned) return;
5489
5527
  this.poisoned = err;
@@ -5665,6 +5703,7 @@ var init_embedding_server = __esm({
5665
5703
  this.server = null;
5666
5704
  }
5667
5705
  if (disposeEmbedder) {
5706
+ await this.queue.waitForIdle();
5668
5707
  await this.embedder.dispose().catch((err) => {
5669
5708
  if (this.isShuttingDown) {
5670
5709
  logger.debug("[EmbeddingServer] Error disposing embedder during shutdown (expected):", err);
@@ -5895,7 +5934,7 @@ var init_embedding_client = __esm({
5895
5934
  }
5896
5935
  }
5897
5936
  // ---- Internal HTTP helpers ----
5898
- getRequest(path19, timeoutMs = 12e4) {
5937
+ getRequest(path20, timeoutMs = 12e4) {
5899
5938
  return new Promise((resolve9, reject) => {
5900
5939
  let resolved = false;
5901
5940
  let req;
@@ -5903,7 +5942,7 @@ var init_embedding_client = __esm({
5903
5942
  if (!resolved) {
5904
5943
  resolved = true;
5905
5944
  req?.destroy();
5906
- reject(new Error(`[EmbeddingClient] GET ${path19} timed out after ${timeoutMs}ms`));
5945
+ reject(new Error(`[EmbeddingClient] GET ${path20} timed out after ${timeoutMs}ms`));
5907
5946
  }
5908
5947
  }, timeoutMs);
5909
5948
  if (timer.unref) timer.unref();
@@ -5911,7 +5950,7 @@ var init_embedding_client = __esm({
5911
5950
  {
5912
5951
  hostname: "127.0.0.1",
5913
5952
  port: this.port,
5914
- path: path19,
5953
+ path: path20,
5915
5954
  method: "GET",
5916
5955
  headers: this.authHeaders()
5917
5956
  },
@@ -5927,19 +5966,19 @@ var init_embedding_client = __esm({
5927
5966
  try {
5928
5967
  const parsed = JSON.parse(collected.toString());
5929
5968
  if (res.statusCode !== 200) {
5930
- reject(new Error(`[EmbeddingClient] GET ${path19} returned HTTP ${res.statusCode}`));
5969
+ reject(new Error(`[EmbeddingClient] GET ${path20} returned HTTP ${res.statusCode}`));
5931
5970
  } else {
5932
5971
  resolve9(parsed);
5933
5972
  }
5934
5973
  } catch (_e) {
5935
- reject(new Error(`[EmbeddingClient] Failed to parse GET ${path19} response: ${collected.toString()}`));
5974
+ reject(new Error(`[EmbeddingClient] Failed to parse GET ${path20} response: ${collected.toString()}`));
5936
5975
  }
5937
5976
  });
5938
5977
  res.on("error", (err) => {
5939
5978
  if (resolved) return;
5940
5979
  resolved = true;
5941
5980
  clearTimeout(timer);
5942
- reject(new Error(`[EmbeddingClient] GET ${path19} response error: ${err.message}`));
5981
+ reject(new Error(`[EmbeddingClient] GET ${path20} response error: ${err.message}`));
5943
5982
  });
5944
5983
  }
5945
5984
  );
@@ -5947,12 +5986,12 @@ var init_embedding_client = __esm({
5947
5986
  if (resolved) return;
5948
5987
  resolved = true;
5949
5988
  clearTimeout(timer);
5950
- reject(new Error(`[EmbeddingClient] GET ${path19} request error: ${err.message}`));
5989
+ reject(new Error(`[EmbeddingClient] GET ${path20} request error: ${err.message}`));
5951
5990
  });
5952
5991
  req.end();
5953
5992
  });
5954
5993
  }
5955
- request(path19, data) {
5994
+ request(path20, data) {
5956
5995
  return new Promise((resolve9, reject) => {
5957
5996
  const timeoutMs = 12e4;
5958
5997
  let resolved = false;
@@ -5961,7 +6000,7 @@ var init_embedding_client = __esm({
5961
6000
  if (!resolved) {
5962
6001
  resolved = true;
5963
6002
  req?.destroy();
5964
- reject(new Error(`[EmbeddingClient] POST ${path19} timed out after ${timeoutMs}ms`));
6003
+ reject(new Error(`[EmbeddingClient] POST ${path20} timed out after ${timeoutMs}ms`));
5965
6004
  }
5966
6005
  }, timeoutMs);
5967
6006
  if (timer.unref) timer.unref();
@@ -5970,7 +6009,7 @@ var init_embedding_client = __esm({
5970
6009
  {
5971
6010
  hostname: "127.0.0.1",
5972
6011
  port: this.port,
5973
- path: path19,
6012
+ path: path20,
5974
6013
  method: "POST",
5975
6014
  headers: {
5976
6015
  "Content-Type": "application/json",
@@ -5996,14 +6035,14 @@ var init_embedding_client = __esm({
5996
6035
  resolve9(parsed);
5997
6036
  }
5998
6037
  } catch (_e) {
5999
- reject(new Error(`[EmbeddingClient] Failed to parse POST ${path19} response: ${collected.toString()}`));
6038
+ reject(new Error(`[EmbeddingClient] Failed to parse POST ${path20} response: ${collected.toString()}`));
6000
6039
  }
6001
6040
  });
6002
6041
  res.on("error", (err) => {
6003
6042
  if (resolved) return;
6004
6043
  resolved = true;
6005
6044
  clearTimeout(timer);
6006
- reject(new Error(`[EmbeddingClient] POST ${path19} response error: ${err.message}`));
6045
+ reject(new Error(`[EmbeddingClient] POST ${path20} response error: ${err.message}`));
6007
6046
  });
6008
6047
  }
6009
6048
  );
@@ -6011,7 +6050,7 @@ var init_embedding_client = __esm({
6011
6050
  if (resolved) return;
6012
6051
  resolved = true;
6013
6052
  clearTimeout(timer);
6014
- reject(new Error(`[EmbeddingClient] POST ${path19} request error: ${err.message}`));
6053
+ reject(new Error(`[EmbeddingClient] POST ${path20} request error: ${err.message}`));
6015
6054
  });
6016
6055
  req.write(body);
6017
6056
  req.end();
@@ -7640,8 +7679,15 @@ var init_store = __esm({
7640
7679
  const canonicalDir = path15.join(this.options.dbDir, `${canonicalName}.lance`);
7641
7680
  await fsPromises.rename(tempDir, canonicalDir);
7642
7681
  this.tableName = canonicalName;
7643
- await this.saveManifest();
7644
- this.table = await this.db.openTable(this.tableName);
7682
+ if (await this.saveManifest()) {
7683
+ this.table = await this.db.openTable(this.tableName);
7684
+ } else {
7685
+ logger.error("[store] Failed to persist canonical table name after rename \u2014 reverting to keep manifest and disk in sync.");
7686
+ await fsPromises.rename(canonicalDir, tempDir);
7687
+ this.tableName = tempTableName;
7688
+ this.table = await this.db.openTable(this.tableName);
7689
+ logger.info(`[store] Migrated data remains safe and accessible as ${tempTableName}; canonical rename deferred to a future successful manifest save.`);
7690
+ }
7645
7691
  } catch (renameErr) {
7646
7692
  logger.warn(`[store] Directory rename failed, persisting temp table name in manifest: ${renameErr}`);
7647
7693
  this.tableName = tempTableName;
@@ -8693,6 +8739,7 @@ var init_chunker = __esm({
8693
8739
 
8694
8740
  // src/knowledge/index.ts
8695
8741
  import * as fs14 from "node:fs";
8742
+ import * as path16 from "node:path";
8696
8743
  function getMigrationStrategy(config) {
8697
8744
  const strategy = config.MIGRATION_STRATEGY;
8698
8745
  if (!strategy) return void 0;
@@ -8754,15 +8801,25 @@ async function createKnowledgeStoreComponents(embedderFactory, reconnectFactory,
8754
8801
  }
8755
8802
  async function forceDeleteKnowledgeStore(config, workspace) {
8756
8803
  const dbDir = getDbDir(config, workspace);
8757
- if (fs14.existsSync(dbDir)) {
8758
- try {
8759
- fs14.rmSync(dbDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8760
- fs14.mkdirSync(dbDir, { recursive: true });
8761
- logger.info(`[knowledge] Knowledge store at ${dbDir} forcefully deleted and recreated.`);
8762
- } catch (err) {
8763
- logger.error(`[knowledge] Failed to forcefully delete directory ${dbDir}:`, err);
8764
- throw err;
8804
+ if (!fs14.existsSync(dbDir)) return;
8805
+ let tableName = "knowledge";
8806
+ const manifestPath = path16.join(dbDir, "store-manifest.json");
8807
+ try {
8808
+ if (fs14.existsSync(manifestPath)) {
8809
+ const manifest = JSON.parse(fs14.readFileSync(manifestPath, "utf-8"));
8810
+ if (manifest.activeTableName) tableName = manifest.activeTableName;
8765
8811
  }
8812
+ } catch (err) {
8813
+ logger.warn(`[knowledge] Failed to read manifest before forced delete, defaulting to table '${tableName}':`, err);
8814
+ }
8815
+ const tableDir = path16.join(dbDir, `${tableName}.lance`);
8816
+ if (!fs14.existsSync(tableDir)) return;
8817
+ try {
8818
+ fs14.rmSync(tableDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
8819
+ logger.info(`[knowledge] Corrupted table directory ${tableDir} forcefully deleted; will be recreated on next init.`);
8820
+ } catch (err) {
8821
+ logger.error(`[knowledge] Failed to forcefully delete table directory ${tableDir}:`, err);
8822
+ throw err;
8766
8823
  }
8767
8824
  }
8768
8825
  var MAX_INIT_RETRIES, BASE_RETRY_DELAY_MS, MAX_RETRY_DELAY_MS;
@@ -8787,7 +8844,7 @@ var knowledge_store_service_exports = {};
8787
8844
  __export(knowledge_store_service_exports, {
8788
8845
  KnowledgeStoreService: () => KnowledgeStoreService
8789
8846
  });
8790
- import * as path16 from "node:path";
8847
+ import * as path17 from "node:path";
8791
8848
  var KnowledgeStoreService;
8792
8849
  var init_knowledge_store_service = __esm({
8793
8850
  "src/infrastructure/knowledge-store-service.ts"() {
@@ -8881,7 +8938,7 @@ var init_knowledge_store_service = __esm({
8881
8938
  };
8882
8939
  const container = tryGetServiceContainerFromCtx(ctx);
8883
8940
  const pathConfig = await getService(ServiceNames.STATE_PATH_CONFIGURATION, void 0, container);
8884
- const lockPath = path16.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
8941
+ const lockPath = path17.join(pathConfig.getLockDirPath(), "knowledge-store-init.lock");
8885
8942
  if (!this._initLock) {
8886
8943
  const processLifecycle = await getService(ServiceNames.PROCESS_LIFECYCLE, void 0, container);
8887
8944
  this._initLock = new FileLockService({
@@ -9942,11 +9999,11 @@ __export(skill_installer_exports, {
9942
9999
  import * as crypto10 from "node:crypto";
9943
10000
  import * as fs16 from "node:fs";
9944
10001
  import * as os6 from "node:os";
9945
- import * as path17 from "node:path";
10002
+ import * as path18 from "node:path";
9946
10003
  import { fileURLToPath as fileURLToPath4 } from "node:url";
9947
10004
  function readSkillVersion(sourceDir) {
9948
10005
  try {
9949
- const md = fs16.readFileSync(path17.join(sourceDir, "SKILL.md"), "utf-8");
10006
+ const md = fs16.readFileSync(path18.join(sourceDir, "SKILL.md"), "utf-8");
9950
10007
  return /"version"\s*:\s*"([^"]+)"/.exec(md)?.[1] ?? null;
9951
10008
  } catch {
9952
10009
  return null;
@@ -9961,7 +10018,7 @@ function resolveSkillSourceDir(explicit) {
9961
10018
  if (process.env["PI_RESEARCH_SKILL_DIR"]) candidates.push(process.env["PI_RESEARCH_SKILL_DIR"]);
9962
10019
  const fromHere = (() => {
9963
10020
  try {
9964
- return path17.dirname(fileURLToPath4(import.meta.url));
10021
+ return path18.dirname(fileURLToPath4(import.meta.url));
9965
10022
  } catch {
9966
10023
  return void 0;
9967
10024
  }
@@ -9970,22 +10027,22 @@ function resolveSkillSourceDir(explicit) {
9970
10027
  if (!start) continue;
9971
10028
  let dir = start;
9972
10029
  for (let i = 0; i < 8; i++) {
9973
- candidates.push(path17.join(dir, "skills", SKILL_NAME));
9974
- const parent = path17.dirname(dir);
10030
+ candidates.push(path18.join(dir, "skills", SKILL_NAME));
10031
+ const parent = path18.dirname(dir);
9975
10032
  if (parent === dir) break;
9976
10033
  dir = parent;
9977
10034
  }
9978
10035
  }
9979
10036
  for (const c of candidates) {
9980
10037
  try {
9981
- if (fs16.existsSync(path17.join(c, "SKILL.md"))) return path17.resolve(c);
10038
+ if (fs16.existsSync(path18.join(c, "SKILL.md"))) return path18.resolve(c);
9982
10039
  } catch {
9983
10040
  }
9984
10041
  }
9985
10042
  throw new Error("Could not locate the bundled research skill (skills/pi-research/SKILL.md). Set PI_RESEARCH_SKILL_DIR to its path.");
9986
10043
  }
9987
10044
  function getManifestPath(opts) {
9988
- return path17.join(homeDir(opts), ".pi", "research", "installed-skills.json");
10045
+ return path18.join(homeDir(opts), ".pi", "research", "installed-skills.json");
9989
10046
  }
9990
10047
  function readManifest(opts) {
9991
10048
  const p = getManifestPath(opts);
@@ -10002,7 +10059,7 @@ function readManifest(opts) {
10002
10059
  }
10003
10060
  function writeManifest(manifest, opts) {
10004
10061
  const p = getManifestPath(opts);
10005
- fs16.mkdirSync(path17.dirname(p), { recursive: true });
10062
+ fs16.mkdirSync(path18.dirname(p), { recursive: true });
10006
10063
  const tmp = `${p}.tmp.${process.pid}.${Date.now()}.${manifestTmpSeq++}`;
10007
10064
  fs16.writeFileSync(tmp, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
10008
10065
  try {
@@ -10024,8 +10081,8 @@ function isOwnedSymlink(targetPath, expectedSource) {
10024
10081
  try {
10025
10082
  const st = fs16.lstatSync(targetPath);
10026
10083
  if (!st.isSymbolicLink()) return false;
10027
- const dest = path17.resolve(path17.dirname(targetPath), fs16.readlinkSync(targetPath));
10028
- if (expectedSource && path17.resolve(expectedSource) === dest) return true;
10084
+ const dest = path18.resolve(path18.dirname(targetPath), fs16.readlinkSync(targetPath));
10085
+ if (expectedSource && path18.resolve(expectedSource) === dest) return true;
10029
10086
  return /[/\\]pi-research[/\\]skills[/\\]pi-research$/.test(dest);
10030
10087
  } catch {
10031
10088
  return false;
@@ -10033,7 +10090,7 @@ function isOwnedSymlink(targetPath, expectedSource) {
10033
10090
  }
10034
10091
  function isOwnedCopy(targetPath) {
10035
10092
  try {
10036
- const md = fs16.readFileSync(path17.join(targetPath, "SKILL.md"), "utf-8");
10093
+ const md = fs16.readFileSync(path18.join(targetPath, "SKILL.md"), "utf-8");
10037
10094
  return md.includes(PACKAGE_NAME);
10038
10095
  } catch {
10039
10096
  return false;
@@ -10042,11 +10099,11 @@ function isOwnedCopy(targetPath) {
10042
10099
  function detectHarnesses(opts) {
10043
10100
  const home = homeDir(opts);
10044
10101
  return HARNESSES.map((h) => {
10045
- const absSkillsDir = path17.join(home, h.skillsDir);
10046
- const absSkillPath = path17.join(absSkillsDir, SKILL_NAME);
10102
+ const absSkillsDir = path18.join(home, h.skillsDir);
10103
+ const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
10047
10104
  let present = false;
10048
10105
  try {
10049
- present = fs16.existsSync(path17.join(home, h.baseDir));
10106
+ present = fs16.existsSync(path18.join(home, h.baseDir));
10050
10107
  } catch {
10051
10108
  }
10052
10109
  let installed = "none";
@@ -10087,15 +10144,15 @@ function installSkill(toolIds, opts = {}) {
10087
10144
  results.push({ tool: id, path: "", type: "symlink", status: "error", message: `unknown target "${id}"` });
10088
10145
  continue;
10089
10146
  }
10090
- const absSkillsDir = path17.join(home, def.skillsDir);
10091
- const absSkillPath = path17.join(absSkillsDir, SKILL_NAME);
10147
+ const absSkillsDir = path18.join(home, def.skillsDir);
10148
+ const absSkillPath = path18.join(absSkillsDir, SKILL_NAME);
10092
10149
  const type = opts.copy ? "copy" : "symlink";
10093
10150
  if (isOwnedSymlink(absSkillPath, source) || isOwnedCopy(absSkillPath)) {
10094
10151
  try {
10095
10152
  const lst = fs16.lstatSync(absSkillPath);
10096
10153
  if (lst.isSymbolicLink()) {
10097
- const dest = path17.resolve(path17.dirname(absSkillPath), fs16.readlinkSync(absSkillPath));
10098
- if (dest !== path17.resolve(source)) {
10154
+ const dest = path18.resolve(path18.dirname(absSkillPath), fs16.readlinkSync(absSkillPath));
10155
+ if (dest !== path18.resolve(source)) {
10099
10156
  if (opts.dryRun) {
10100
10157
  results.push({ tool: id, path: absSkillPath, type: "symlink", status: "planned", message: "would re-point stale symlink to current source" });
10101
10158
  continue;
@@ -10171,7 +10228,7 @@ function uninstallSkill(toolIds, opts = {}) {
10171
10228
  }
10172
10229
  }
10173
10230
  for (const { tool, path: skillPath } of targets.values()) {
10174
- if (toolIds && toolIds.length && !toolIds.includes(tool)) {
10231
+ if (toolIds !== void 0 && !toolIds.includes(tool)) {
10175
10232
  const e = manifest.entries.find((x) => x.path === skillPath);
10176
10233
  if (e) keep.push(e);
10177
10234
  continue;
@@ -10232,19 +10289,19 @@ function reconcileSkillInstalls(opts = {}) {
10232
10289
  if (e.type !== "symlink") {
10233
10290
  const stale2 = currentVersion !== null && e.version !== currentVersion;
10234
10291
  if (stale2 && source !== null && fs16.existsSync(e.path) && isOwnedCopy(e.path)) {
10235
- const dir2 = path17.dirname(e.path);
10236
- const base2 = path17.basename(e.path);
10292
+ const dir2 = path18.dirname(e.path);
10293
+ const base2 = path18.basename(e.path);
10237
10294
  try {
10238
10295
  for (const name of fs16.readdirSync(dir2)) {
10239
10296
  if (name.startsWith(`.${base2}.new-`) || name.startsWith(`.${base2}.old-`)) {
10240
- fs16.rmSync(path17.join(dir2, name), { recursive: true, force: true });
10297
+ fs16.rmSync(path18.join(dir2, name), { recursive: true, force: true });
10241
10298
  }
10242
10299
  }
10243
10300
  } catch {
10244
10301
  }
10245
10302
  const suffix = crypto10.randomBytes(4).toString("hex");
10246
- const fresh = path17.join(dir2, `.${base2}.new-${suffix}`);
10247
- const retired = path17.join(dir2, `.${base2}.old-${suffix}`);
10303
+ const fresh = path18.join(dir2, `.${base2}.new-${suffix}`);
10304
+ const retired = path18.join(dir2, `.${base2}.old-${suffix}`);
10248
10305
  try {
10249
10306
  copyDir(source, fresh);
10250
10307
  } catch {
@@ -10290,12 +10347,12 @@ function reconcileSkillInstalls(opts = {}) {
10290
10347
  }
10291
10348
  continue;
10292
10349
  }
10293
- const dir = path17.dirname(e.path);
10294
- const base = path17.basename(e.path);
10350
+ const dir = path18.dirname(e.path);
10351
+ const base = path18.basename(e.path);
10295
10352
  try {
10296
10353
  for (const name of fs16.readdirSync(dir)) {
10297
10354
  if (name.startsWith(`.${base}.new-`)) {
10298
- fs16.rmSync(path17.join(dir, name), { recursive: true, force: true });
10355
+ fs16.rmSync(path18.join(dir, name), { recursive: true, force: true });
10299
10356
  }
10300
10357
  }
10301
10358
  } catch {
@@ -10311,10 +10368,10 @@ function reconcileSkillInstalls(opts = {}) {
10311
10368
  const targetLive = fs16.existsSync(e.path);
10312
10369
  let dest = null;
10313
10370
  try {
10314
- dest = path17.resolve(path17.dirname(e.path), fs16.readlinkSync(e.path));
10371
+ dest = path18.resolve(path18.dirname(e.path), fs16.readlinkSync(e.path));
10315
10372
  } catch {
10316
10373
  }
10317
- const stale = source !== null && dest !== null && dest !== path17.resolve(source);
10374
+ const stale = source !== null && dest !== null && dest !== path18.resolve(source);
10318
10375
  if (!targetLive || stale) {
10319
10376
  if (source !== null) {
10320
10377
  try {
@@ -10329,7 +10386,7 @@ function reconcileSkillInstalls(opts = {}) {
10329
10386
  result.repointed.push(e.path);
10330
10387
  } catch {
10331
10388
  const suffix = crypto10.randomBytes(4).toString("hex");
10332
- const fresh = path17.join(dir, `.${base}.new-${suffix}`);
10389
+ const fresh = path18.join(dir, `.${base}.new-${suffix}`);
10333
10390
  try {
10334
10391
  copyDir(source, fresh);
10335
10392
  fs16.renameSync(fresh, e.path);
@@ -10371,7 +10428,7 @@ function copyDir(src, dest) {
10371
10428
  if (st.isDirectory()) {
10372
10429
  fs16.mkdirSync(dest, { recursive: true });
10373
10430
  for (const name of fs16.readdirSync(src)) {
10374
- copyDir(path17.join(src, name), path17.join(dest, name));
10431
+ copyDir(path18.join(src, name), path18.join(dest, name));
10375
10432
  }
10376
10433
  } else {
10377
10434
  fs16.copyFileSync(src, dest);
@@ -10382,12 +10439,12 @@ var init_skill_installer = __esm({
10382
10439
  "src/skill-install/skill-installer.ts"() {
10383
10440
  "use strict";
10384
10441
  HARNESSES = [
10385
- { id: "claude", label: "Claude", baseDir: ".claude", skillsDir: path17.join(".claude", "skills"), confidence: "confirmed" },
10386
- { id: "openclaw", label: "OpenClaw", baseDir: ".openclaw", skillsDir: path17.join(".openclaw", "skills"), confidence: "confirmed", note: "OpenClaw reads ~/.openclaw/skills as a managed skill root and accepts symlinked skill folders (docs.openclaw.ai/tools/skills)." },
10387
- { id: "pi", label: "pi", baseDir: ".pi", skillsDir: path17.join(".pi", "skills"), confidence: "confirmed" },
10388
- { id: "cursor", label: "Cursor", baseDir: ".cursor", skillsDir: path17.join(".cursor", "skills"), confidence: "partial", note: "Cursor project-level skills are documented; the global ~/.cursor/skills path is community-reported." },
10389
- { id: "codex", label: "OpenAI Codex CLI", baseDir: ".codex", skillsDir: path17.join(".codex", "skills"), confidence: "unverified", note: "Codex skills support is emerging; path not confirmed by official docs." },
10390
- { id: "agents", label: "Cross-tool (~/.agents/skills)", baseDir: ".agents", skillsDir: path17.join(".agents", "skills"), confidence: "unverified", note: "Proposed Universal Agents convention; not yet a ratified standard." }
10442
+ { id: "claude", label: "Claude", baseDir: ".claude", skillsDir: path18.join(".claude", "skills"), confidence: "confirmed" },
10443
+ { id: "openclaw", label: "OpenClaw", baseDir: ".openclaw", skillsDir: path18.join(".openclaw", "skills"), confidence: "confirmed", note: "OpenClaw reads ~/.openclaw/skills as a managed skill root and accepts symlinked skill folders (docs.openclaw.ai/tools/skills)." },
10444
+ { id: "pi", label: "pi", baseDir: ".pi", skillsDir: path18.join(".pi", "skills"), confidence: "confirmed" },
10445
+ { id: "cursor", label: "Cursor", baseDir: ".cursor", skillsDir: path18.join(".cursor", "skills"), confidence: "partial", note: "Cursor project-level skills are documented; the global ~/.cursor/skills path is community-reported." },
10446
+ { id: "codex", label: "OpenAI Codex CLI", baseDir: ".codex", skillsDir: path18.join(".codex", "skills"), confidence: "unverified", note: "Codex skills support is emerging; path not confirmed by official docs." },
10447
+ { id: "agents", label: "Cross-tool (~/.agents/skills)", baseDir: ".agents", skillsDir: path18.join(".agents", "skills"), confidence: "unverified", note: "Proposed Universal Agents convention; not yet a ratified standard." }
10391
10448
  ];
10392
10449
  SKILL_NAME = "pi-research";
10393
10450
  PACKAGE_NAME = "@lincoln504/pi-research";
@@ -10400,7 +10457,7 @@ var init_skill_installer = __esm({
10400
10457
  // src/cli.ts
10401
10458
  import * as fs17 from "node:fs";
10402
10459
  import * as os7 from "node:os";
10403
- import * as path18 from "node:path";
10460
+ import * as path19 from "node:path";
10404
10461
  import { fileURLToPath as fileURLToPath5 } from "node:url";
10405
10462
 
10406
10463
  // src/core/service-initialization.ts
@@ -10814,7 +10871,7 @@ function delimiterRegex(word) {
10814
10871
  return new RegExp(`(^|\\n)([ \\t]*(?:#{1,6}[ \\t]*|>[ \\t]*|\\*\\*[ \\t]*)?)${word}\\b`, "i");
10815
10872
  }
10816
10873
  function splitCoverageDigest(report) {
10817
- if (!report) return { digest: "", body: report ?? "" };
10874
+ if (!report) return { digest: "", body: report };
10818
10875
  const headerMatch = delimiterRegex(COVERAGE_DIGEST_HEADER).exec(report);
10819
10876
  if (!headerMatch) return { digest: "", body: report };
10820
10877
  const lineStart = headerMatch.index + headerMatch[1].length;
@@ -11540,14 +11597,24 @@ ${previousQueries.map((q) => `- ${q}`).join("\n")}
11540
11597
  complexity,
11541
11598
  observer
11542
11599
  };
11543
- const responseText = isRouter ? await this.callLead({
11544
- ...callOptions,
11545
- systemPrompt: populate(""),
11546
- userMessage: buildRouterMessage(...this.buildRouterEvidence(options, round), runContext),
11547
- maxTokens: ROUTER_MAX_TOKENS,
11548
- component: "router",
11549
- label: "router-updatePlanForRound"
11550
- }) : await this.synthesizeCorpus({ ...callOptions, reports, runContext, populate });
11600
+ const responseText = isRouter ? await (() => {
11601
+ const routerMessage = buildRouterMessage(...this.buildRouterEvidence(options, round), runContext);
11602
+ const window = model.contextWindow ?? DEFAULT_MODEL_CONTEXT_WINDOW;
11603
+ const estimatedTokens = routerMessage.length / CHARS_PER_TOKEN + ROUTER_MAX_TOKENS + SYNTHESIS_OVERHEAD_TOKENS;
11604
+ if (estimatedTokens > window) {
11605
+ logger.warn(
11606
+ `[PlanningService] Router request for ${model.id} is an estimated ${Math.round(estimatedTokens)} tokens against a ${window}-token context window (evidence ${routerMessage.length} chars, output ceiling ${ROUTER_MAX_TOKENS} tokens). Unlike the synthesizer, the router has no reduce path for an over-budget round \u2014 sending it anyway, the provider may reject this request.`
11607
+ );
11608
+ }
11609
+ return this.callLead({
11610
+ ...callOptions,
11611
+ systemPrompt: populate(""),
11612
+ userMessage: routerMessage,
11613
+ maxTokens: ROUTER_MAX_TOKENS,
11614
+ component: "router",
11615
+ label: "router-updatePlanForRound"
11616
+ });
11617
+ })() : await this.synthesizeCorpus({ ...callOptions, reports, runContext, populate });
11551
11618
  let plan = null;
11552
11619
  try {
11553
11620
  plan = this.parseJsonPlan(responseText);
@@ -11795,6 +11862,13 @@ ${previousQueries.map((q) => `- ${q}`).join("\n")}
11795
11862
  `[PlanningService] Synthesis corpus is still ${finalSize} chars after ${MAX_SYNTHESIS_REDUCE_PASSES} reduce pass(es), over the ${budgetChars}-char budget for ${args.model.id}. Sending it anyway \u2014 the provider may reject this request.`
11796
11863
  );
11797
11864
  }
11865
+ const window = args.model.contextWindow ?? DEFAULT_MODEL_CONTEXT_WINDOW;
11866
+ const estimatedTotalTokens = (finalSize + globalSourceList.length + runContext.length) / CHARS_PER_TOKEN + config.SYNTHESIS_MAX_TOKENS + SYNTHESIS_OVERHEAD_TOKENS;
11867
+ if (estimatedTotalTokens > window) {
11868
+ logger.warn(
11869
+ `[PlanningService] Synthesis request for ${args.model.id} is an estimated ${Math.round(estimatedTotalTokens)} tokens against a ${window}-token context window (corpus ${finalSize} chars, source list ${globalSourceList.length} chars, run context ${runContext.length} chars, output ceiling ${config.SYNTHESIS_MAX_TOKENS} tokens) \u2014 the source list and/or run context are large enough on their own to overflow the window even though the reduced corpus fits its budget. Sending it anyway \u2014 the provider may reject this request.`
11870
+ );
11871
+ }
11798
11872
  return this.callLead({
11799
11873
  ...args,
11800
11874
  systemPrompt: populate(reduced ? MERGE_SYNTHESIS_SECTION : ""),
@@ -12175,10 +12249,13 @@ function isFullMockMode() {
12175
12249
  }
12176
12250
  var HEALTHCHECK_MIN_BUDGET_MS = 45e3 + 6e4;
12177
12251
  var HEALTHCHECK_NAV_ATTEMPTS = 3;
12252
+ var BROWSER_LAUNCH_TIMEOUT_MS = 9e4;
12253
+ var CONTEXT_CREATION_TIMEOUT_MS = 3e4;
12254
+ var COLD_START_ALLOWANCE_MS = BROWSER_LAUNCH_TIMEOUT_MS + CONTEXT_CREATION_TIMEOUT_MS;
12178
12255
  function getHealthCheckBudgetMs(config) {
12179
12256
  const c = config || getConfig();
12180
12257
  const ms = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
12181
- const longestSlotHold = Math.max(ms(c.SEARCH_TIMEOUT_MS, 45e3), ms(c.SCRAPE_TIMEOUT_MS, 15e3)) + ms(c.BROWSER_TASK_TIMEOUT_MS, 1e4);
12258
+ const longestSlotHold = Math.max(ms(c.SEARCH_TIMEOUT_MS, 45e3), ms(c.SCRAPE_TIMEOUT_MS, 15e3)) + ms(c.BROWSER_TASK_TIMEOUT_MS, 1e4) + COLD_START_ALLOWANCE_MS;
12182
12259
  const probeWork = HEALTHCHECK_NAV_ATTEMPTS * Math.max(1e4, ms(c.HEALTH_CHECK_TIMEOUT_MS, 1e4));
12183
12260
  return Math.max(HEALTHCHECK_MIN_BUDGET_MS, longestSlotHold + probeWork);
12184
12261
  }
@@ -12234,9 +12311,9 @@ var BrowserClient = class {
12234
12311
  get targetPort() {
12235
12312
  return this.port;
12236
12313
  }
12237
- async request(path19, data, signal, config) {
12314
+ async request(path20, data, signal, config) {
12238
12315
  const start = Date.now();
12239
- const operation = path19.includes("/search") ? "search" : path19.includes("/scrape") ? "browser-task" : path19.includes("/healthcheck") ? "healthcheck" : "network";
12316
+ const operation = path20.includes("/search") ? "search" : path20.includes("/scrape") ? "browser-task" : path20.includes("/healthcheck") ? "healthcheck" : "network";
12240
12317
  return new Promise((resolve9, reject) => {
12241
12318
  const agent = getClientAgent();
12242
12319
  const timeoutMs = resolveClientRequestTimeoutMs(operation, config);
@@ -12261,7 +12338,7 @@ var BrowserClient = class {
12261
12338
  resolved = true;
12262
12339
  abortCleanup?.();
12263
12340
  controller.abort();
12264
- const error = new Error(`[BrowserClient] Request to ${path19} timed out after ${timeoutMs}ms (Shared queue may be deep)`);
12341
+ const error = new Error(`[BrowserClient] Request to ${path20} timed out after ${timeoutMs}ms (Shared queue may be deep)`);
12265
12342
  errorTracker.trackError(error, {
12266
12343
  component: "browser-manager",
12267
12344
  operation,
@@ -12274,7 +12351,7 @@ var BrowserClient = class {
12274
12351
  const req = http4.request({
12275
12352
  hostname: "127.0.0.1",
12276
12353
  port: this.port,
12277
- path: path19,
12354
+ path: path20,
12278
12355
  method: "POST",
12279
12356
  agent,
12280
12357
  signal: controller.signal,
@@ -12298,7 +12375,7 @@ var BrowserClient = class {
12298
12375
  const error = new Error(
12299
12376
  `Browser pool leader is draining (${parsed.reason || "leadership-lost"}) \u2014 re-elect and retry`
12300
12377
  );
12301
- logger.debug(`[BrowserClient] Leader draining on ${path19}; re-election required.`);
12378
+ logger.debug(`[BrowserClient] Leader draining on ${path20}; re-election required.`);
12302
12379
  errorTracker.trackError(error, {
12303
12380
  component: "browser-manager",
12304
12381
  operation,
@@ -12314,7 +12391,7 @@ var BrowserClient = class {
12314
12391
  });
12315
12392
  reject(error);
12316
12393
  } else {
12317
- logger.debug(`[BrowserClient] Request ${path19} completed in ${duration}ms`);
12394
+ logger.debug(`[BrowserClient] Request ${path20} completed in ${duration}ms`);
12318
12395
  resolve9(parsed);
12319
12396
  }
12320
12397
  } catch (_e) {
@@ -12334,7 +12411,7 @@ var BrowserClient = class {
12334
12411
  resolved = true;
12335
12412
  clearTimeout(timer);
12336
12413
  abortCleanup?.();
12337
- const error = new Error(`[BrowserClient] Response stream error on ${path19}: ${err.message}`);
12414
+ const error = new Error(`[BrowserClient] Response stream error on ${path20}: ${err.message}`);
12338
12415
  errorTracker.trackError(error, {
12339
12416
  component: "browser-manager",
12340
12417
  operation,
@@ -12353,20 +12430,20 @@ var BrowserClient = class {
12353
12430
  let errorMsg;
12354
12431
  let errorType;
12355
12432
  if (nodeErr.code === "ECONNRESET" || nodeErr.code === "EPIPE") {
12356
- errorMsg = `Browser pool socket ${path19} closed (pool likely busy or restarting) - ${err.message}`;
12433
+ errorMsg = `Browser pool socket ${path20} closed (pool likely busy or restarting) - ${err.message}`;
12357
12434
  errorType = "connection_reset";
12358
12435
  } else if (nodeErr.code === "ECONNREFUSED") {
12359
- errorMsg = `Browser pool ${path19} unreachable (server restarting or torn down) - ${err.message}`;
12436
+ errorMsg = `Browser pool ${path20} unreachable (server restarting or torn down) - ${err.message}`;
12360
12437
  errorType = "connection_refused";
12361
12438
  } else if (nodeErr.code === "ETIMEDOUT") {
12362
- errorMsg = `Browser pool ${path19} timed out (slow browser response) - ${err.message}`;
12439
+ errorMsg = `Browser pool ${path20} timed out (slow browser response) - ${err.message}`;
12363
12440
  errorType = "timeout";
12364
12441
  } else {
12365
- errorMsg = `Browser pool ${path19} error: ${err.message}`;
12442
+ errorMsg = `Browser pool ${path20} error: ${err.message}`;
12366
12443
  errorType = "unknown";
12367
12444
  }
12368
12445
  const error = new Error(errorMsg);
12369
- logger.error(`[BrowserClient] Request to http://127.0.0.1:${this.port}${path19} failed:`, errorMsg);
12446
+ logger.error(`[BrowserClient] Request to http://127.0.0.1:${this.port}${path20} failed:`, errorMsg);
12370
12447
  errorTracker.trackError(error, {
12371
12448
  component: "browser-manager",
12372
12449
  operation,
@@ -13246,7 +13323,7 @@ var BrowserTaskScheduler = class {
13246
13323
  const pool = await (await this.getWorkerPoolManager()).ensurePool(config);
13247
13324
  const startTime = Date.now();
13248
13325
  const cfg = config || getConfig();
13249
- const timeoutMs = cfg.SEARCH_TIMEOUT_MS + cfg.BROWSER_TASK_TIMEOUT_MS;
13326
+ const timeoutMs = cfg.SEARCH_TIMEOUT_MS + cfg.BROWSER_TASK_TIMEOUT_MS + COLD_START_ALLOWANCE_MS;
13250
13327
  let timeoutId;
13251
13328
  let dispatchedAt = 0;
13252
13329
  let startDeadline = () => {
@@ -13335,7 +13412,7 @@ var BrowserTaskScheduler = class {
13335
13412
  const startTime = Date.now();
13336
13413
  const cfg = config || getConfig();
13337
13414
  const isMocking = process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true";
13338
- const timeoutMs = cfg.SCRAPE_TIMEOUT_MS + (isMocking ? 5e3 : cfg.BROWSER_TASK_TIMEOUT_MS);
13415
+ const timeoutMs = cfg.SCRAPE_TIMEOUT_MS + (isMocking ? 5e3 : cfg.BROWSER_TASK_TIMEOUT_MS + COLD_START_ALLOWANCE_MS);
13339
13416
  let timeoutId;
13340
13417
  let dispatchedAt = 0;
13341
13418
  let startDeadline = () => {
@@ -15393,8 +15470,8 @@ var StateValidator = class {
15393
15470
  if (!Value4.Check(SingletonStateSchema, coerced)) {
15394
15471
  const errors = [...Value4.Errors(SingletonStateSchema, coerced)];
15395
15472
  const errorMsg = errors.map((e) => {
15396
- const path19 = String(e.path || "root");
15397
- return `${path19}: ${e.message} (${JSON.stringify(e.value)})`;
15473
+ const path20 = String(e.path || "root");
15474
+ return `${path20}: ${e.message} (${JSON.stringify(e.value)})`;
15398
15475
  }).join(", ");
15399
15476
  throw new Error(`Invalid state: ${errorMsg}`);
15400
15477
  }
@@ -17123,7 +17200,7 @@ async function performSearch(queries, config, signal, onProgress, container = ge
17123
17200
  metrics.increment("browser_search_orchestrations_total", 1);
17124
17201
  logger.log(`[Search] Orchestrating ${queries.length} queries across ${maxWorkers} worker processes...`);
17125
17202
  if (onProgress) onProgress(0);
17126
- const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4);
17203
+ const QUERY_TIMEOUT_MS = (config?.SEARCH_TIMEOUT_MS ?? 45e3) + (config?.BROWSER_TASK_TIMEOUT_MS ?? 1e4) + COLD_START_ALLOWANCE_MS;
17127
17204
  const filteredQueries = [...new Set(queries.filter((q) => q.trim()))];
17128
17205
  let timeoutCount = 0;
17129
17206
  let errorCount = 0;
@@ -18158,28 +18235,31 @@ async function extractPdfToMarkdown(bytes) {
18158
18235
  }
18159
18236
  try {
18160
18237
  const doc = new WasmPdfDocument(bytes);
18161
- const pageCount = doc.pageCount();
18162
- let markdown = `# PDF Document
18238
+ try {
18239
+ const pageCount = doc.pageCount();
18240
+ let markdown = `# PDF Document
18163
18241
 
18164
18242
  **Pages:** ${pageCount}
18165
18243
 
18166
18244
  `;
18167
- try {
18168
- markdown += doc.toMarkdownAll();
18169
- } catch {
18170
- for (let i = 0; i < pageCount; i++) {
18171
- markdown += `## Page ${i + 1}
18245
+ try {
18246
+ markdown += doc.toMarkdownAll();
18247
+ } catch {
18248
+ for (let i = 0; i < pageCount; i++) {
18249
+ markdown += `## Page ${i + 1}
18172
18250
 
18173
18251
  ${doc.toMarkdown(i)}
18174
18252
 
18175
18253
  `;
18254
+ }
18176
18255
  }
18256
+ const pdfExtractionDuration = Date.now() - pdfExtractionStart;
18257
+ metrics.observe("scrape_pdf_conversion_ms", pdfExtractionDuration);
18258
+ metrics.increment("scrape_pdf_conversions_total", 1, { status: "success", pages: String(pageCount) });
18259
+ return markdown;
18260
+ } finally {
18261
+ doc.free();
18177
18262
  }
18178
- doc.free();
18179
- const pdfExtractionDuration = Date.now() - pdfExtractionStart;
18180
- metrics.observe("scrape_pdf_conversion_ms", pdfExtractionDuration);
18181
- metrics.increment("scrape_pdf_conversions_total", 1, { status: "success", pages: String(pageCount) });
18182
- return markdown;
18183
18263
  } catch (e) {
18184
18264
  const msg2 = e instanceof Error ? e.message : String(e);
18185
18265
  logger.debug(`[Scrapers] PDF extraction failed: ${msg2}`);
@@ -18279,7 +18359,7 @@ async function scrapeWithFetch(url, signal) {
18279
18359
  const contentLength = response.headers.get("content-length");
18280
18360
  if (contentLength) {
18281
18361
  const size = parseInt(contentLength, 10);
18282
- if (contentType.includes("application/pdf") || url.toLowerCase().endsWith(".pdf")) {
18362
+ if (contentType.includes("application/pdf") || currentUrl.toLowerCase().endsWith(".pdf")) {
18283
18363
  if (size > MAX_PDF_SIZE) {
18284
18364
  const sizeMB = Math.round(size / 1024 / 1024);
18285
18365
  logger.warn(`[Scrapers] PDF too large (Content-Length: ${sizeMB}MB, max 100MB), skipping`);
@@ -18301,7 +18381,7 @@ async function scrapeWithFetch(url, signal) {
18301
18381
  throw new Error(`HTML response too large (${sizeMB}MB, max 25MB)`);
18302
18382
  }
18303
18383
  }
18304
- if (contentType.includes("application/pdf") || url.toLowerCase().endsWith(".pdf")) {
18384
+ if (contentType.includes("application/pdf") || currentUrl.toLowerCase().endsWith(".pdf")) {
18305
18385
  let pdfBytes;
18306
18386
  try {
18307
18387
  pdfBytes = await readBodyCapped(response, MAX_PDF_SIZE);
@@ -20426,14 +20506,6 @@ function createSecuritySearchTool(options) {
20426
20506
  details: { error: "invalid_parameters" }
20427
20507
  };
20428
20508
  }
20429
- const allowed = options.tracker.recordCall("security_search");
20430
- if (!allowed) {
20431
- metrics.increment("tool_security_search_calls_total", 1, { status: "rate_limited" });
20432
- return {
20433
- content: [{ type: "text", text: options.tracker.getLimitMessage("security_search") }],
20434
- details: { blocked: true, reason: "limit_reached" }
20435
- };
20436
- }
20437
20509
  const p = params;
20438
20510
  const terms = p.terms;
20439
20511
  if (terms.length === 0) {
@@ -20475,6 +20547,14 @@ function createSecuritySearchTool(options) {
20475
20547
  }
20476
20548
  }
20477
20549
  const maxResults = p.maxResults ?? 20;
20550
+ const allowed = options.tracker.recordCall("security_search");
20551
+ if (!allowed) {
20552
+ metrics.increment("tool_security_search_calls_total", 1, { status: "rate_limited" });
20553
+ return {
20554
+ content: [{ type: "text", text: options.tracker.getLimitMessage("security_search") }],
20555
+ details: { blocked: true, reason: "limit_reached" }
20556
+ };
20557
+ }
20478
20558
  metrics.increment("tool_security_search_terms_total", terms.length);
20479
20559
  metrics.increment("tool_security_search_databases_total", databases.length);
20480
20560
  let results;
@@ -20882,20 +20962,19 @@ var StackExchangeClient = class {
20882
20962
  }
20883
20963
  });
20884
20964
  }
20965
+ // Pure accessor — no metrics side effects. `stackexchangeCommand()` calls this
20966
+ // repeatedly per invocation (pre-request gate checks, success footer, error
20967
+ // footer) purely to read current state for display; incrementing counters here
20968
+ // used to record 2-3x the real number of quota-low/exhausted events per call.
20969
+ // The counters are incremented once, at the single gate check that actually
20970
+ // observes each event (see stackexchange/index.ts).
20885
20971
  getQuotaInfo() {
20886
- const info = {
20972
+ return {
20887
20973
  remaining: this.quotaRemaining,
20888
20974
  max: this.quotaMax,
20889
20975
  requestCount: this.requestCount,
20890
20976
  lastBackoff: this.lastBackoff
20891
20977
  };
20892
- if (this.isQuotaExhausted()) {
20893
- metrics.increment("stackexchange_quota_exhausted_total", 1);
20894
- }
20895
- if (this.isQuotaLow()) {
20896
- metrics.increment("stackexchange_quota_low_total", 1);
20897
- }
20898
- return info;
20899
20978
  }
20900
20979
  isQuotaExhausted() {
20901
20980
  return this.quotaExhaustedAt !== null && Date.now() - this.quotaExhaustedAt < QUOTA_PROBE_COOLDOWN_MS;
@@ -21174,6 +21253,7 @@ function formatSitesCompact(sites) {
21174
21253
 
21175
21254
  // src/stackexchange/index.ts
21176
21255
  init_logger();
21256
+ init_metrics();
21177
21257
  function notify(ctx, message, type) {
21178
21258
  ctx.ui?.notify?.(message, type);
21179
21259
  }
@@ -21200,6 +21280,7 @@ async function stackexchangeCommand(options) {
21200
21280
  const client = getClient(config);
21201
21281
  try {
21202
21282
  if (client.isQuotaExhausted()) {
21283
+ metrics.increment("stackexchange_quota_exhausted_total", 1);
21203
21284
  const quota2 = client.getQuotaInfo();
21204
21285
  notify(
21205
21286
  ctx,
@@ -21215,6 +21296,7 @@ async function stackexchangeCommand(options) {
21215
21296
  };
21216
21297
  }
21217
21298
  if (client.isQuotaLow()) {
21299
+ metrics.increment("stackexchange_quota_low_total", 1);
21218
21300
  const quota2 = client.getQuotaInfo();
21219
21301
  notify(
21220
21302
  ctx,
@@ -21663,6 +21745,7 @@ init_http_body();
21663
21745
  init_logger();
21664
21746
  init_safe_unref();
21665
21747
  init_metrics();
21748
+ init_http_body();
21666
21749
  var DEFAULT_REQUEST_KEY = "O43z0dpjhgX20SCx4KAo";
21667
21750
  var BRIDGED_GLOBALS = ["window", "document", "location", "navigator", "origin"];
21668
21751
  function describeError(error) {
@@ -21762,7 +21845,10 @@ async function doMint(identifiers, opts) {
21762
21845
  body: JSON.stringify([requestKey, botguardResponse]),
21763
21846
  signal: opts.signal
21764
21847
  });
21765
- const integrityJson = await integrityResponse.json();
21848
+ if (!integrityResponse.ok) {
21849
+ throw new Error(`HTTP ${integrityResponse.status} from attestation server`);
21850
+ }
21851
+ const integrityJson = await readJsonCapped(integrityResponse);
21766
21852
  const integrityToken = integrityJson?.[0];
21767
21853
  if (!integrityToken) {
21768
21854
  throw new Error("Integrity token rejected by attestation server (likely a non-residential IP)");
@@ -21817,7 +21903,16 @@ async function fetchVideoTranscripts(videoIds, opts = {}) {
21817
21903
  if (log?.setLevel && log.Level) log.setLevel(log.Level.NONE);
21818
21904
  } catch {
21819
21905
  }
21820
- const fetchImpl = (input, init) => fetch(input, init);
21906
+ const ssrfFetcher = await getSsrfSafeFetcher();
21907
+ const fetchImpl = ssrfFetcher ? (input, init) => {
21908
+ if (typeof input === "string" || input instanceof URL) {
21909
+ return ssrfFetcher.fetch(String(input), {
21910
+ ...init ?? {},
21911
+ dispatcher: ssrfFetcher.dispatcher
21912
+ });
21913
+ }
21914
+ return fetch(input, init);
21915
+ } : (input, init) => fetch(input, init);
21821
21916
  let visitorData;
21822
21917
  try {
21823
21918
  const seed = await breaker.execute(() => withTimeout2(
@@ -21900,10 +21995,27 @@ async function fetchOne(session, videoId, contentToken, lang, timeoutMs, signal,
21900
21995
  const sep3 = track.base_url.includes("?") ? "&" : "?";
21901
21996
  const url = `${track.base_url}${sep3}fmt=json3&c=WEB&pot=${encodeURIComponent(contentToken)}`;
21902
21997
  const fetchSignal = createTimeoutSignal(timeoutMs, signal);
21903
- const response = await fetchImpl(url, {
21904
- signal: fetchSignal,
21905
- headers: { "User-Agent": "Mozilla/5.0", "Accept-Language": lang }
21906
- });
21998
+ const MAX_REDIRECTS = 5;
21999
+ let currentUrl = url;
22000
+ let response;
22001
+ for (let hop = 0; hop <= MAX_REDIRECTS; hop++) {
22002
+ response = await fetchImpl(currentUrl, {
22003
+ signal: fetchSignal,
22004
+ redirect: "manual",
22005
+ headers: { "User-Agent": "Mozilla/5.0", "Accept-Language": lang }
22006
+ });
22007
+ if (response.status >= 300 && response.status < 400) {
22008
+ const location = response.headers.get("location");
22009
+ if (!location) break;
22010
+ const resolved = new URL(location, currentUrl).href;
22011
+ assertTrustedTimedtextUrl(resolved);
22012
+ void response.body?.cancel()?.catch(() => {
22013
+ });
22014
+ currentUrl = resolved;
22015
+ continue;
22016
+ }
22017
+ break;
22018
+ }
21907
22019
  if (!response.ok) {
21908
22020
  void response.body?.cancel()?.catch(() => {
21909
22021
  });
@@ -23898,7 +24010,6 @@ var DeepResearchOrchestrator = class {
23898
24010
  if (!routerChoseSynthesis) {
23899
24011
  if (this.currentRound > lastAnnouncedRound) {
23900
24012
  observer?.onRoundStart?.(this.currentRound);
23901
- lastAnnouncedRound = this.currentRound;
23902
24013
  }
23903
24014
  observer?.onEvaluationStart?.(this.currentRound);
23904
24015
  observer?.onEvaluationProgress?.("evaluating");
@@ -25303,6 +25414,10 @@ async function _doInit(options = {}) {
25303
25414
  }
25304
25415
  }
25305
25416
  async function initResearchSDK(options = {}) {
25417
+ if (_shutdownPromise) {
25418
+ await _shutdownPromise.catch(() => {
25419
+ });
25420
+ }
25306
25421
  if (isInitialized) {
25307
25422
  logger.warn("[SDK] Research SDK already initialized. Call shutdownResearchSDK() first if you want to re-initialize with new options.");
25308
25423
  return;
@@ -25653,7 +25768,7 @@ init_logger();
25653
25768
  init_constants();
25654
25769
  import { promises as fs15 } from "node:fs";
25655
25770
  import { homedir as homedir4, tmpdir as tmpdir4, platform as platform3 } from "node:os";
25656
- import { join as join19, resolve as resolve5, normalize, sep as sep2 } from "node:path";
25771
+ import { join as join20, resolve as resolve5, normalize, sep as sep2 } from "node:path";
25657
25772
  import { randomBytes as randomBytes6 } from "node:crypto";
25658
25773
  var PREFERRED_SUBDIRS = [
25659
25774
  "research",
@@ -25724,7 +25839,7 @@ async function findPreferredSubdir(cwd) {
25724
25839
  for (const target of PREFERRED_SUBDIRS) {
25725
25840
  const match = entries.find((e) => e.toLowerCase() === target);
25726
25841
  if (match === void 0) continue;
25727
- const candidate = join19(cwd, match);
25842
+ const candidate = join20(cwd, match);
25728
25843
  try {
25729
25844
  const stat6 = await fs15.stat(candidate);
25730
25845
  if (stat6.isDirectory()) return candidate;
@@ -25769,7 +25884,7 @@ async function exportResearchReport(query, result, _mode, cwd, explicitDir) {
25769
25884
  for (let attempt = 0; attempt < MAX_EXPORT_RETRIES; attempt++) {
25770
25885
  const hash = generateHash();
25771
25886
  const filename = `${baseFilename}-${hash}.md`;
25772
- const filepath = join19(targetDir, filename);
25887
+ const filepath = join20(targetDir, filename);
25773
25888
  try {
25774
25889
  await fs15.writeFile(filepath, result, { flag: "wx" });
25775
25890
  logger.log(`[export] Research report saved to: ${filepath}`);
@@ -25966,8 +26081,8 @@ function resolvedConfigPaths() {
25966
26081
  configDir,
25967
26082
  cliIfaceEnv: getInterfaceEnvFilePath("cli"),
25968
26083
  piIfaceEnv: getInterfaceEnvFilePath("pi"),
25969
- piAuth: path18.join(agentDir, "auth.json"),
25970
- piModels: path18.join(agentDir, "models.json"),
26084
+ piAuth: path19.join(agentDir, "auth.json"),
26085
+ piModels: path19.join(agentDir, "models.json"),
25971
26086
  piState: getStateDir()
25972
26087
  };
25973
26088
  }
@@ -26940,8 +27055,8 @@ Error: ${err.message}
26940
27055
  var _isMain = (() => {
26941
27056
  try {
26942
27057
  if (!process.argv[1]) return false;
26943
- const modulePath = fs17.realpathSync(path18.resolve(fileURLToPath5(import.meta.url)));
26944
- const invokedPath = fs17.realpathSync(path18.resolve(process.argv[1]));
27058
+ const modulePath = fs17.realpathSync(path19.resolve(fileURLToPath5(import.meta.url)));
27059
+ const invokedPath = fs17.realpathSync(path19.resolve(process.argv[1]));
26945
27060
  return modulePath === invokedPath;
26946
27061
  } catch {
26947
27062
  return false;