@lincoln504/pi-research 1.3.3 → 1.3.5

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 (39) hide show
  1. package/README.md +7 -2
  2. package/dist/cli.mjs +242 -95
  3. package/dist/thread-worker.mjs +122 -50
  4. package/docs/CHANGELOG.md +76 -0
  5. package/package.json +1 -1
  6. package/skills/pi-research/SKILL.md +1 -1
  7. package/src/core/service-registry.ts +37 -3
  8. package/src/healthcheck/index.ts +9 -7
  9. package/src/infrastructure/browser/browser-client.ts +5 -2
  10. package/src/infrastructure/browser/task-execution-service.ts +21 -5
  11. package/src/infrastructure/browser/thread-worker-browser.ts +49 -1
  12. package/src/infrastructure/browser/thread-worker-messaging.ts +70 -25
  13. package/src/infrastructure/browser/thread-worker.mjs +122 -50
  14. package/src/infrastructure/knowledge-store-service.ts +27 -2
  15. package/src/knowledge/chunker.ts +14 -1
  16. package/src/orchestration/quick-research-orchestrator.ts +13 -2
  17. package/src/orchestration/research-orchestration-service.ts +16 -3
  18. package/src/sdk.ts +11 -2
  19. package/src/security/github-advisories.ts +13 -4
  20. package/src/security/osv-types.ts +1 -1
  21. package/src/skill-install/skill-installer.ts +69 -6
  22. package/src/stackexchange/index.ts +15 -0
  23. package/src/stackexchange/rest-client.ts +13 -24
  24. package/src/tools/research-tool-definition.ts +3 -2
  25. package/src/tools/scrape.ts +6 -4
  26. package/src/tools/security.ts +13 -4
  27. package/src/utils/input-validation.ts +0 -10
  28. package/src/utils/json-utils.ts +0 -78
  29. package/src/utils/log-rotation.ts +21 -6
  30. package/src/utils/log-utils.ts +43 -33
  31. package/src/utils/metrics.ts +0 -8
  32. package/src/utils/shared-links.ts +0 -14
  33. package/src/utils/stdio-capture.ts +44 -7
  34. package/src/utils/text-utils.ts +6 -2
  35. package/src/utils/url-utils.ts +7 -1
  36. package/src/web-research/scraper-utils.ts +8 -3
  37. package/src/web-research/web-scraper.ts +33 -1
  38. package/src/youtube/potoken.ts +26 -2
  39. package/src/youtube/transcript-client.ts +5 -2
package/dist/cli.mjs CHANGED
@@ -215,12 +215,8 @@ function formatArg(arg) {
215
215
  return String(arg);
216
216
  }
217
217
  function redactSecrets(message) {
218
- let keep = MAX_LOG_MESSAGE_LENGTH;
219
- if (message.length > keep) {
220
- const lastKeptUnit = message.charCodeAt(keep - 1);
221
- if (lastKeptUnit >= 55296 && lastKeptUnit <= 56319) keep -= 1;
222
- }
223
- let out = message.length > MAX_LOG_MESSAGE_LENGTH ? `${message.slice(0, keep)}\u2026[truncated ${message.length - keep} chars]` : message;
218
+ const rawLength = message.length;
219
+ let out = rawLength > REDACT_SCAN_LENGTH ? message.slice(0, REDACT_SCAN_LENGTH) : message;
224
220
  out = out.replace(ANSI_PATTERN, "");
225
221
  out = out.replace(URL_CREDENTIALS_PATTERN, "$1[REDACTED]@");
226
222
  out = out.replace(JWT_PATTERN, "[REDACTED]");
@@ -231,12 +227,20 @@ function redactSecrets(message) {
231
227
  out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
232
228
  out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
233
229
  out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
230
+ let keep = MAX_LOG_MESSAGE_LENGTH;
231
+ if (out.length > keep) {
232
+ const lastKeptUnit = out.charCodeAt(keep - 1);
233
+ if (lastKeptUnit >= 55296 && lastKeptUnit <= 56319) keep -= 1;
234
+ }
235
+ if (out.length > MAX_LOG_MESSAGE_LENGTH) {
236
+ out = `${out.slice(0, keep)}\u2026[truncated ${rawLength - keep} chars]`;
237
+ }
234
238
  return out;
235
239
  }
236
240
  function stripTerminalEscapes(message) {
237
241
  return message.replace(TERMINAL_ESCAPE_PATTERN, "").replace(CONTROL_CHARS_PATTERN, " ");
238
242
  }
239
- 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, TERMINAL_ESCAPE_PATTERN;
243
+ 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, TERMINAL_ESCAPE_PATTERN;
240
244
  var init_log_utils = __esm({
241
245
  "src/utils/log-utils.ts"() {
242
246
  "use strict";
@@ -245,7 +249,7 @@ var init_log_utils = __esm({
245
249
  ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
246
250
  URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
247
251
  COOKIE_HEADER_PATTERN = /\b(set[_-]?cookie|cookie)\b(["']?\s*[:=]\s*["']?)([^\r\n]+)/gi;
248
- 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)\b(["']?\s*[:=]\s*["']?)([^\s"',&)]+)/gi;
252
+ 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;
249
253
  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;
250
254
  JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
251
255
  BASIC_AUTH_PATTERN = /\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}/gi;
@@ -253,6 +257,7 @@ var init_log_utils = __esm({
253
257
  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;
254
258
  LONG_HEX_SECRET_PATTERN = /\b[0-9a-fA-F]{32,}\b/g;
255
259
  CONTROL_CHARS_PATTERN = /[\x00-\x1f\x7f]/g;
260
+ REDACT_SCAN_LENGTH = MAX_LOG_MESSAGE_LENGTH * 4;
256
261
  TERMINAL_ESCAPE_PATTERN = new RegExp(
257
262
  "\\x1b\\[[0-?]*[ -/]*[@-~]?|\\x1b\\][^\\x07\\x1b]*(?:\\x07|\\x1b\\\\)?|\\x1b[PX^_][^\\x1b]*(?:\\x1b\\\\)?|\\x1b.?",
258
263
  "g"
@@ -263,10 +268,17 @@ var init_log_utils = __esm({
263
268
  // src/utils/log-rotation.ts
264
269
  import * as fs from "node:fs";
265
270
  import * as path2 from "node:path";
266
- var LogRotation;
271
+ function touchOwnerOnly(filePath, mode) {
272
+ try {
273
+ fs.closeSync(fs.openSync(filePath, APPEND_CREATE_NOFOLLOW_FLAGS, mode));
274
+ } catch {
275
+ }
276
+ }
277
+ var APPEND_CREATE_NOFOLLOW_FLAGS, LogRotation;
267
278
  var init_log_rotation = __esm({
268
279
  "src/utils/log-rotation.ts"() {
269
280
  "use strict";
281
+ APPEND_CREATE_NOFOLLOW_FLAGS = fs.constants.O_WRONLY | fs.constants.O_APPEND | fs.constants.O_CREAT | (typeof fs.constants.O_NOFOLLOW === "number" ? fs.constants.O_NOFOLLOW : 0);
270
282
  LogRotation = class {
271
283
  MAX_LOG_SIZE = 10 * 1024 * 1024;
272
284
  // 10MB max file size
@@ -284,10 +296,7 @@ var init_log_rotation = __esm({
284
296
  try {
285
297
  if (fs.existsSync(logFile)) {
286
298
  fs.unlinkSync(logFile);
287
- try {
288
- fs.closeSync(fs.openSync(logFile, "a", 384));
289
- } catch {
290
- }
299
+ touchOwnerOnly(logFile, 384);
291
300
  try {
292
301
  fs.chmodSync(logFile, 384);
293
302
  } catch {
@@ -327,10 +336,7 @@ var init_log_rotation = __esm({
327
336
  this.logger.log("[Logger] Log rotation skipped (rename failed; file likely in use):", renameErr);
328
337
  return;
329
338
  }
330
- try {
331
- fs.closeSync(fs.openSync(logFile, "a", 384));
332
- } catch {
333
- }
339
+ touchOwnerOnly(logFile, 384);
334
340
  try {
335
341
  fs.chmodSync(logFile, 384);
336
342
  } catch {
@@ -497,7 +503,10 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
497
503
  unkeyedCaptureDepth++;
498
504
  isAnyLoggerCapturingOutput = true;
499
505
  }
500
- const decoder = new TextDecoder2();
506
+ const stderrDecoder = new TextDecoder2();
507
+ const stdoutDecoder = new TextDecoder2();
508
+ const fd1SyncDecoder = new TextDecoder2();
509
+ const fd2SyncDecoder = new TextDecoder2();
501
510
  const originalStderrWrite = process.stderr.write;
502
511
  const originalStdoutWrite = process.stdout.write;
503
512
  const originalFsWriteSync = fs3.writeSync;
@@ -543,7 +552,11 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
543
552
  console.debug = createConsolePatch("debug", logFile, hasSufficientDiskSpace);
544
553
  process.stderr.write = (chunk, encodingOrCb, callback) => {
545
554
  const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
546
- const message = typeof chunk === "string" ? chunk : decoder.decode(chunk);
555
+ const message = typeof chunk === "string" ? chunk : stderrDecoder.decode(chunk, { stream: true });
556
+ if (message.length === 0) {
557
+ if (typeof cb === "function") cb();
558
+ return true;
559
+ }
547
560
  if (!hasSufficientDiskSpace()) {
548
561
  if (typeof cb === "function") cb();
549
562
  return true;
@@ -564,7 +577,7 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
564
577
  return true;
565
578
  };
566
579
  process.stdout.write = (chunk, encodingOrCb, callback) => {
567
- const message = typeof chunk === "string" ? chunk : decoder.decode(chunk);
580
+ const message = typeof chunk === "string" ? chunk : stdoutDecoder.decode(chunk, { stream: true });
568
581
  const isNative = isNativeLog(message);
569
582
  const complexTui = isComplexTui(message);
570
583
  const boxDrawing = isBoxDrawing(message);
@@ -618,7 +631,11 @@ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
618
631
  if (!descriptor || (descriptor.writable || descriptor.set)) {
619
632
  fs3.writeSync = (fd, chunk, ...args) => {
620
633
  if (fd === 1 || fd === 2) {
621
- const message = typeof chunk === "string" ? chunk : decoder.decode(chunk);
634
+ const fdDecoder = fd === 1 ? fd1SyncDecoder : fd2SyncDecoder;
635
+ const message = typeof chunk === "string" ? chunk : fdDecoder.decode(chunk, { stream: true });
636
+ if (fd === 2 && message.length === 0) {
637
+ return typeof chunk === "string" ? Buffer.from(chunk).length : chunk.length;
638
+ }
622
639
  const isNative = isNativeLog(message);
623
640
  const boxDrawing = isBoxDrawing(message);
624
641
  const complexTui = isComplexTui(message);
@@ -1349,6 +1366,24 @@ var init_service_registry = __esm({
1349
1366
  ]);
1350
1367
  if (settleTimer) clearTimeout(settleTimer);
1351
1368
  }
1369
+ const inFlightDisposals = Array.from(this.services.values()).map((r) => r.disposalPromise).filter((p) => p != null);
1370
+ if (inFlightDisposals.length > 0) {
1371
+ if (this.defaultOptions.enableLogging) {
1372
+ logger.debug(`[ServiceContainer] Waiting for ${inFlightDisposals.length} in-flight disposal(s) before teardown`);
1373
+ }
1374
+ let disposalSettleTimer;
1375
+ await Promise.race([
1376
+ Promise.allSettled(inFlightDisposals),
1377
+ new Promise((resolve9) => {
1378
+ disposalSettleTimer = setTimeout(() => {
1379
+ logger.warn("[ServiceContainer] Timed out waiting for in-flight service disposal; proceeding with teardown");
1380
+ resolve9();
1381
+ }, DISPOSE_INFLIGHT_INIT_TIMEOUT_MS);
1382
+ disposalSettleTimer.unref?.();
1383
+ })
1384
+ ]);
1385
+ if (disposalSettleTimer) clearTimeout(disposalSettleTimer);
1386
+ }
1352
1387
  const activeServices = Array.from(this.services.keys()).filter(
1353
1388
  (name) => this.services.get(name)?.instance !== null
1354
1389
  );
@@ -1932,7 +1967,7 @@ function normalizeUrl(url) {
1932
1967
  }
1933
1968
  return result;
1934
1969
  } catch (_err) {
1935
- logger.debug(`[url-utils] normalizing unparseable URL (${url.slice(0, 200)}): ${_err instanceof Error ? _err.message : String(_err)}`);
1970
+ logger.debug(`[url-utils] normalizing unparseable URL (${redactSecrets(url).slice(0, 200)}): ${_err instanceof Error ? _err.message : String(_err)}`);
1936
1971
  let cleaned = url.trim().replace(/^[*_~`"']{1,20}/, "").replace(/[*_~`"',.)}\]]{1,20}$/, "");
1937
1972
  const hashIdx = cleaned.indexOf("#");
1938
1973
  if (hashIdx >= 0) cleaned = cleaned.substring(0, hashIdx);
@@ -1996,6 +2031,7 @@ var init_url_utils = __esm({
1996
2031
  "src/utils/url-utils.ts"() {
1997
2032
  "use strict";
1998
2033
  init_logger();
2034
+ init_log_utils();
1999
2035
  MAX_INITIAL_LINKS = 20;
2000
2036
  MAX_INITIAL_LINK_CHARS = 2048;
2001
2037
  }
@@ -6434,7 +6470,7 @@ var init_chunker = __esm({
6434
6470
  if (code >= 56320 && code <= 57343) {
6435
6471
  if (start - 1 > iterationStart) {
6436
6472
  start -= 1;
6437
- } else if (start + 1 < end) {
6473
+ } else if (start + 1 <= end) {
6438
6474
  start += 1;
6439
6475
  }
6440
6476
  }
@@ -8663,6 +8699,9 @@ var init_knowledge_store_service = __esm({
8663
8699
  await this._initializationPromise;
8664
8700
  } catch {
8665
8701
  }
8702
+ if (this._disposalPromise) {
8703
+ return this._disposalPromise;
8704
+ }
8666
8705
  }
8667
8706
  this.lifecycle = "disposing" /* DISPOSING */;
8668
8707
  logger.debug("[KnowledgeStoreService] Disposing...");
@@ -8670,7 +8709,8 @@ var init_knowledge_store_service = __esm({
8670
8709
  const store = this._store;
8671
8710
  const writerQueue = this._writerQueue;
8672
8711
  const initLock = this._initLock;
8673
- this._disposalPromise = (async () => {
8712
+ let disposePromise = null;
8713
+ disposePromise = (async () => {
8674
8714
  try {
8675
8715
  if (writerQueue) {
8676
8716
  await writerQueue.dispose?.();
@@ -8694,9 +8734,12 @@ var init_knowledge_store_service = __esm({
8694
8734
  logger.error("[KnowledgeStoreService] Error during disposal:", err);
8695
8735
  } finally {
8696
8736
  this.lifecycle = "disposed" /* DISPOSED */;
8697
- this._disposalPromise = null;
8737
+ if (this._disposalPromise === disposePromise) {
8738
+ this._disposalPromise = null;
8739
+ }
8698
8740
  }
8699
8741
  })();
8742
+ this._disposalPromise = disposePromise;
8700
8743
  return this._disposalPromise;
8701
8744
  }
8702
8745
  /**
@@ -9740,16 +9783,31 @@ function installSkill(toolIds, opts = {}) {
9740
9783
  continue;
9741
9784
  }
9742
9785
  fs16.unlinkSync(absSkillPath);
9743
- fs16.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
9744
- upsertEntry(manifest, { tool: id, path: absSkillPath, type: "symlink", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
9745
- results.push({ tool: id, path: absSkillPath, type: "symlink", status: "installed", message: "re-pointed stale symlink to current source" });
9786
+ try {
9787
+ fs16.symlinkSync(source, absSkillPath, process.platform === "win32" ? "junction" : "dir");
9788
+ upsertEntry(manifest, { tool: id, path: absSkillPath, type: "symlink", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
9789
+ results.push({ tool: id, path: absSkillPath, type: "symlink", status: "installed", message: "re-pointed stale symlink to current source" });
9790
+ } catch (relinkErr) {
9791
+ try {
9792
+ copyDir(source, absSkillPath);
9793
+ upsertEntry(manifest, { tool: id, path: absSkillPath, type: "copy", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
9794
+ results.push({ tool: id, path: absSkillPath, type: "copy", status: "installed", message: `symlink re-point unavailable (${relinkErr?.code ?? relinkErr?.message}); copied instead` });
9795
+ } catch (copyErr) {
9796
+ results.push({ tool: id, path: absSkillPath, type: "symlink", status: "error", message: `symlink re-point failed (${relinkErr?.code ?? relinkErr?.message}) and the copy fallback also failed: ${copyErr?.message ?? String(copyErr)}` });
9797
+ }
9798
+ }
9746
9799
  continue;
9747
9800
  }
9748
9801
  }
9749
9802
  } catch {
9750
9803
  }
9751
9804
  results.push({ tool: id, path: absSkillPath, type, status: "already-installed" });
9752
- upsertEntry(manifest, { tool: id, path: absSkillPath, type: isSymlinkPresent(absSkillPath) ? "symlink" : "copy", source, createdAt: now, ...skillVersion ? { version: skillVersion } : {} });
9805
+ {
9806
+ const isSymlinkNow = isSymlinkPresent(absSkillPath);
9807
+ const existingEntry = manifest.entries.find((e) => e.path === absSkillPath);
9808
+ const versionField = isSymlinkNow ? skillVersion ? { version: skillVersion } : {} : existingEntry?.version ? { version: existingEntry.version } : {};
9809
+ upsertEntry(manifest, { tool: id, path: absSkillPath, type: isSymlinkNow ? "symlink" : "copy", source, createdAt: now, ...versionField });
9810
+ }
9753
9811
  continue;
9754
9812
  }
9755
9813
  if (fs16.existsSync(absSkillPath) || isSymlinkPresent(absSkillPath)) {
@@ -9933,11 +9991,31 @@ function reconcileSkillInstalls(opts = {}) {
9933
9991
  if (source !== null) {
9934
9992
  try {
9935
9993
  fs16.unlinkSync(e.path);
9994
+ } catch {
9995
+ keep.push(e);
9996
+ continue;
9997
+ }
9998
+ try {
9936
9999
  fs16.symlinkSync(source, e.path, process.platform === "win32" ? "junction" : "dir");
9937
10000
  keep.push({ ...e, type: "symlink", source });
9938
10001
  result.repointed.push(e.path);
9939
10002
  } catch {
9940
- keep.push(e);
10003
+ const dir = path17.dirname(e.path);
10004
+ const base = path17.basename(e.path);
10005
+ const suffix = crypto10.randomBytes(4).toString("hex");
10006
+ const fresh = path17.join(dir, `.${base}.new-${suffix}`);
10007
+ try {
10008
+ copyDir(source, fresh);
10009
+ fs16.renameSync(fresh, e.path);
10010
+ keep.push({ ...e, type: "copy", source });
10011
+ result.repointed.push(e.path);
10012
+ } catch {
10013
+ try {
10014
+ fs16.rmSync(fresh, { recursive: true, force: true });
10015
+ } catch {
10016
+ }
10017
+ result.pruned.push(e.path);
10018
+ }
9941
10019
  }
9942
10020
  } else if (!targetLive) {
9943
10021
  try {
@@ -11384,6 +11462,7 @@ function isFullMockMode() {
11384
11462
  init_config();
11385
11463
  init_logger();
11386
11464
  init_error_tracker();
11465
+ init_log_utils();
11387
11466
  init_http_body();
11388
11467
  import * as http2 from "node:http";
11389
11468
 
@@ -11519,7 +11598,7 @@ var BrowserClient = class {
11519
11598
  resolve9(parsed);
11520
11599
  }
11521
11600
  } catch (_e) {
11522
- const raw = collected.toString();
11601
+ const raw = redactSecrets(collected.toString());
11523
11602
  const preview = raw.length > 200 ? raw.slice(0, 200) + "..." : raw;
11524
11603
  const error = new Error(`Failed to parse response (status ${res.statusCode}): ${preview}`);
11525
11604
  errorTracker.trackError(error, {
@@ -15867,6 +15946,7 @@ init_logger();
15867
15946
 
15868
15947
  // src/infrastructure/browser/task-execution-service.ts
15869
15948
  init_logger();
15949
+ init_log_utils();
15870
15950
  init_error_tracker();
15871
15951
 
15872
15952
  // src/infrastructure/browser/browser-lifecycle.ts
@@ -15914,6 +15994,10 @@ var lastRestartTime = 0;
15914
15994
  var RESTART_COOLDOWN_MS = 1e4;
15915
15995
  var LEADER_HANDOVER_RETRIES = 3;
15916
15996
  var HANDOVER_BACKOFF_MS = 1200;
15997
+ function briefErrorMessage(error) {
15998
+ const msg2 = error instanceof Error ? error.message : String(error);
15999
+ return redactSecrets(msg2).substring(0, 100);
16000
+ }
15917
16001
  async function recoverFromLeaderHandover(container) {
15918
16002
  logger.warn("[BrowserManager] Leader handover detected (pool draining) \u2014 dropping stale scheduler handle and re-resolving...");
15919
16003
  await forceSchedulerRestart2(false, container).catch(
@@ -16003,7 +16087,7 @@ async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retrie
16003
16087
  taskType: type,
16004
16088
  errorType: "leader_handover"
16005
16089
  });
16006
- logger.warn(`[BrowserManager] Leader handover during ${type} task (handover attempts left: ${handoverRetries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
16090
+ logger.warn(`[BrowserManager] Leader handover during ${type} task (handover attempts left: ${handoverRetries}): ${briefErrorMessage(error)}...`);
16007
16091
  await recoverFromLeaderHandover(container);
16008
16092
  await new Promise((resolve9) => setTimeout(resolve9, HANDOVER_BACKOFF_MS + Math.floor(Math.random() * 400)));
16009
16093
  return runBrowserTask(taskOrUrl, type, config, signal, retries, container, handoverRetries - 1);
@@ -16015,7 +16099,7 @@ async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retrie
16015
16099
  taskType: type,
16016
16100
  errorType: "transient_socket_error"
16017
16101
  });
16018
- logger.warn(`[BrowserManager] Transient socket error during ${type} task (retries left: ${retries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
16102
+ logger.warn(`[BrowserManager] Transient socket error during ${type} task (retries left: ${retries}): ${briefErrorMessage(error)}...`);
16019
16103
  if (isPoolShutdownError(error)) {
16020
16104
  logger.warn(`[BrowserManager] Pool is draining \u2014 waiting for pool idle before retry...`);
16021
16105
  await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
@@ -16042,7 +16126,7 @@ async function runBrowserHealthCheck(config, retries = 1, signal, container = ge
16042
16126
  operation: "healthcheck",
16043
16127
  errorType: "transient_socket_error"
16044
16128
  });
16045
- logger.warn(`[BrowserManager] Transient socket error during healthcheck (retries left: ${retries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
16129
+ logger.warn(`[BrowserManager] Transient socket error during healthcheck (retries left: ${retries}): ${briefErrorMessage(error)}...`);
16046
16130
  if (isPoolShutdownError(error)) {
16047
16131
  await recoverFromLeaderHandover(container);
16048
16132
  } else {
@@ -16074,7 +16158,7 @@ async function runWorkerSearch(query, config, signal, retries = 1, sessionId, co
16074
16158
  query,
16075
16159
  errorType: "leader_handover"
16076
16160
  });
16077
- logger.warn(`[BrowserManager] Leader handover during search (handover attempts left: ${handoverRetries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
16161
+ logger.warn(`[BrowserManager] Leader handover during search (handover attempts left: ${handoverRetries}): ${briefErrorMessage(error)}...`);
16078
16162
  await recoverFromLeaderHandover(container);
16079
16163
  await new Promise((resolve9) => setTimeout(resolve9, HANDOVER_BACKOFF_MS + Math.floor(Math.random() * 400)));
16080
16164
  return runWorkerSearch(query, config, signal, retries, sessionId, container, handoverRetries - 1);
@@ -16086,7 +16170,7 @@ async function runWorkerSearch(query, config, signal, retries = 1, sessionId, co
16086
16170
  query,
16087
16171
  errorType: "transient_socket_error"
16088
16172
  });
16089
- logger.warn(`[BrowserManager] Transient socket error during search (retries left: ${retries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
16173
+ logger.warn(`[BrowserManager] Transient socket error during search (retries left: ${retries}): ${briefErrorMessage(error)}...`);
16090
16174
  if (isPoolShutdownError(error)) {
16091
16175
  await waitForBrowserPoolIdle(15e3).catch((err) => logger.debug("Wait for browser idle timed out or failed:", err));
16092
16176
  } else {
@@ -16513,13 +16597,11 @@ function registerHealthChecks(registry, container = getServiceContainer()) {
16513
16597
  registry.register("StateManager", async () => {
16514
16598
  try {
16515
16599
  const stateManager = await getService(ServiceNames.STATE_MANAGER, { container }, container);
16516
- const stats = await stateManager.getMetrics();
16517
16600
  const gpuOwner = await stateManager.getGpuOwner();
16518
16601
  return {
16519
16602
  healthy: true,
16520
16603
  diagnostic: {
16521
16604
  status: "operational",
16522
- sessions: stats.activeSessions,
16523
16605
  gpuLocked: !!gpuOwner,
16524
16606
  gpuOwner: gpuOwner?.sessionId || "none"
16525
16607
  }
@@ -16805,6 +16887,7 @@ function isBenignScrapeFailure(error) {
16805
16887
  return msg2.startsWith("Fetch blocked:") || // bot-protection interstitial (Cloudflare, DDoS-Guard, …)
16806
16888
  msg2.startsWith("Fetch returned stub:") || // nav-only / near-empty page
16807
16889
  msg2.startsWith("Could not extract content from PDF") || // malformed/encrypted/scanned-only PDF
16890
+ msg2.startsWith("PDF too large") || // exceeds the deliberate 100MB policy cap
16808
16891
  /^HTTP \d{3}\b/.test(msg2) || // remote 4xx/5xx status
16809
16892
  isCloudflareBlockError(error) || isTaskTimeoutError(error) || isPoolShutdownError(error);
16810
16893
  }
@@ -17097,8 +17180,22 @@ async function extractPdfToMarkdown(bytes) {
17097
17180
  throw new Error(`PDF too large (${sizeMB}MB, max 100MB)`);
17098
17181
  }
17099
17182
  const pdfExtractionStart = Date.now();
17183
+ let WasmPdfDocument;
17184
+ try {
17185
+ ({ WasmPdfDocument } = await import("pdf-oxide-wasm"));
17186
+ } catch (e) {
17187
+ const msg2 = e instanceof Error ? e.message : String(e);
17188
+ logger.error(`[Scrapers] pdf-oxide-wasm failed to load \u2014 PDF extraction is unavailable (not just for this URL): ${msg2}`);
17189
+ metrics.increment("scrape_pdf_errors_total", 1, { error_type: "native_module_unavailable" });
17190
+ errorTracker.trackError(e instanceof Error ? e : String(e), {
17191
+ component: "scrapers",
17192
+ operation: "pdf-extract",
17193
+ contentType: "pdf",
17194
+ errorType: "native_module_unavailable"
17195
+ });
17196
+ throw new Error(`PDF extraction unavailable (pdf-oxide-wasm failed to load: ${msg2})`, { cause: e });
17197
+ }
17100
17198
  try {
17101
- const { WasmPdfDocument } = await import("pdf-oxide-wasm");
17102
17199
  const doc = new WasmPdfDocument(bytes);
17103
17200
  const pageCount = doc.pageCount();
17104
17201
  let markdown = `# PDF Document
@@ -17472,6 +17569,7 @@ async function scrape(urls, maxConcurrency = 5, signal, config, sessionId, onUrl
17472
17569
  // src/tools/scrape.ts
17473
17570
  init_constants();
17474
17571
  init_text_utils();
17572
+ init_log_utils();
17475
17573
  init_config();
17476
17574
  init_service_registry();
17477
17575
  init_service_interfaces();
@@ -17643,18 +17741,19 @@ ${evictedDuplicates.map((u) => `- ${u}`).join("\n")}
17643
17741
  let freshResults;
17644
17742
  const concurrency = p.maxConcurrency || defaultConcurrency;
17645
17743
  try {
17646
- const results = await scrape(urlsToFetch, concurrency, signal, options.config, getGlobalState().researchId, (result) => {
17744
+ const results = await scrape(urlsToFetch, concurrency, signal, config, getGlobalState().researchId, (result) => {
17647
17745
  options.onUrlScrapeResult?.(result.url, result.success);
17648
17746
  }, container);
17649
17747
  freshResults = Array.isArray(results) ? results : [];
17650
17748
  } catch (error) {
17651
- logger.error(`[scrape tool] Scrape failed: ${error instanceof Error ? error.message : String(error)}`);
17749
+ const message = error instanceof Error ? error.message : String(error);
17750
+ logger.error(`[scrape tool] Scrape failed: ${message}`);
17652
17751
  metrics.increment("tool_scrape_calls_total", 1, { status: "error" });
17653
17752
  return {
17654
17753
  content: [{ type: "text", text: `# Scrape Failed
17655
17754
 
17656
- ${error instanceof Error ? error.message : String(error)}` }],
17657
- details: { error: String(error) }
17755
+ ${redactSecrets(message)}` }],
17756
+ details: { error: redactSecrets(String(error)) }
17658
17757
  };
17659
17758
  }
17660
17759
  const successfulFresh = freshResults.filter((r) => r.success);
@@ -18548,9 +18647,9 @@ async function searchGitHubAdvisories(terms, options) {
18548
18647
  throw new Error(`Invalid repo format: "${options.repo}". Expected "owner/name".`);
18549
18648
  }
18550
18649
  const [owner, name] = repoParts;
18551
- const repoNameRe = /^[A-Za-z0-9._-]+$/;
18650
+ const repoNameRe = /^(?!\.+$)[A-Za-z0-9._-]+$/;
18552
18651
  if (!repoNameRe.test(owner) || !repoNameRe.test(name)) {
18553
- throw new Error(`Invalid repo format: "${options.repo}". Owner and name may only contain letters, digits, '.', '_' and '-'.`);
18652
+ throw new Error(`Invalid repo format: "${options.repo}". Owner and name may only contain letters, digits, '.', '_' and '-', and cannot be all dots.`);
18554
18653
  }
18555
18654
  const url = `${GITHUB_API_BASE}/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/security-advisories?per_page=${perPage}`;
18556
18655
  const response = await githubCircuitBreaker.execute(() => retryWithBackoff(async () => {
@@ -18901,7 +19000,7 @@ function mapOsvItemToVulnerability(item) {
18901
19000
  for (const cwe of item.database_specific.cwe) {
18902
19001
  if (typeof cwe === "string") {
18903
19002
  cwes.push(cwe);
18904
- } else if (typeof cwe === "object" && typeof cwe.id === "string") {
19003
+ } else if (typeof cwe === "object" && cwe !== null && typeof cwe.id === "string") {
18905
19004
  cwes.push(cwe.id);
18906
19005
  }
18907
19006
  }
@@ -19282,6 +19381,10 @@ async function searchSecurityDatabases(params, signal) {
19282
19381
  // src/tools/security.ts
19283
19382
  init_constants();
19284
19383
  init_metrics();
19384
+ var MAX_DESCRIPTION_CHARS = 300;
19385
+ function truncateDescription(description) {
19386
+ return description.length > MAX_DESCRIPTION_CHARS ? `${description.substring(0, MAX_DESCRIPTION_CHARS)}...` : description;
19387
+ }
19285
19388
  function createSecuritySearchTool(options) {
19286
19389
  const SecuritySearchParamsSchema = Type6.Object({
19287
19390
  databases: Type6.Optional(Type6.Array(Type6.String({
@@ -19484,7 +19587,7 @@ Unable to search security databases. This may be a temporary issue - try again l
19484
19587
  }
19485
19588
  }
19486
19589
  const description = vuln.description;
19487
- markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}
19590
+ markdown += `- **Description:** ${truncateDescription(description)}
19488
19591
  `;
19489
19592
  if (vuln.knownExploited === true) {
19490
19593
  markdown += "- **[Actively Exploited]**\n";
@@ -19536,7 +19639,7 @@ Unable to search security databases. This may be a temporary issue - try again l
19536
19639
  `;
19537
19640
  }
19538
19641
  const description = vuln.description;
19539
- markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}
19642
+ markdown += `- **Description:** ${truncateDescription(description)}
19540
19643
  `;
19541
19644
  if (vuln.dueDate !== void 0) {
19542
19645
  markdown += `- **Due Date:** ${vuln.dueDate}
@@ -19584,7 +19687,7 @@ Unable to search security databases. This may be a temporary issue - try again l
19584
19687
  markdown += `- **Summary:** ${adv.summary}
19585
19688
  `;
19586
19689
  const description = adv.description ?? "";
19587
- markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}
19690
+ markdown += `- **Description:** ${truncateDescription(description)}
19588
19691
  `;
19589
19692
  if (adv.published) {
19590
19693
  markdown += `- **Published:** ${adv.published}
@@ -19626,7 +19729,7 @@ Unable to search security databases. This may be a temporary issue - try again l
19626
19729
  markdown += `- **Severity:** ${vuln.severity}
19627
19730
  `;
19628
19731
  const description = vuln.description;
19629
- markdown += `- **Description:** ${description.length > 300 ? `${description.substring(0, 300)}...` : description}
19732
+ markdown += `- **Description:** ${truncateDescription(description)}
19630
19733
  `;
19631
19734
  if (vuln.affectedProducts && vuln.affectedProducts.length > 0) {
19632
19735
  markdown += `- **Affected:** ${vuln.affectedProducts.join(", ")}
@@ -19674,6 +19777,7 @@ init_logger();
19674
19777
  init_circuit_breaker();
19675
19778
  init_metrics();
19676
19779
  init_http_body();
19780
+ init_retry_utils();
19677
19781
  var API_BASE = "https://api.stackexchange.com/2.3";
19678
19782
  var QUOTA_PROBE_COOLDOWN_MS = 10 * 60 * 1e3;
19679
19783
  var StackExchangeClient = class {
@@ -19721,21 +19825,11 @@ var StackExchangeClient = class {
19721
19825
  if (!url.searchParams.has("site") && !options.endpoint.startsWith("/sites")) {
19722
19826
  url.searchParams.set("site", "stackoverflow.com");
19723
19827
  }
19724
- const controller = new AbortController();
19725
- const timeoutId = setTimeout(() => {
19726
- controller.abort();
19727
- }, this._timeout);
19728
- const abortHandler = () => {
19729
- clearTimeout(timeoutId);
19730
- controller.abort();
19731
- };
19732
- if (signal) {
19733
- signal.addEventListener("abort", abortHandler, { once: true });
19734
- }
19828
+ const requestSignal = createTimeoutSignal(this._timeout, signal);
19735
19829
  try {
19736
19830
  const response = await fetch(url.toString(), {
19737
19831
  method: options.method,
19738
- signal: controller.signal,
19832
+ signal: requestSignal,
19739
19833
  headers: {
19740
19834
  "Accept": "application/json"
19741
19835
  }
@@ -19790,11 +19884,6 @@ var StackExchangeClient = class {
19790
19884
  throw new Error(`Request timeout after ${this._timeout}ms`, { cause: error });
19791
19885
  }
19792
19886
  throw error;
19793
- } finally {
19794
- clearTimeout(timeoutId);
19795
- if (signal) {
19796
- signal.removeEventListener("abort", abortHandler);
19797
- }
19798
19887
  }
19799
19888
  });
19800
19889
  }
@@ -20035,6 +20124,33 @@ function formatCompactQuestions(questions) {
20035
20124
  }
20036
20125
 
20037
20126
  // src/stackexchange/output/compact.ts
20127
+ function formatQuestionsCompact(questions) {
20128
+ if (questions.length === 0) {
20129
+ return "No questions found.";
20130
+ }
20131
+ const lines = [];
20132
+ let index = 1;
20133
+ for (const q of questions) {
20134
+ const accepted = q.accepted_answer_id ? "[accepted]" : " ";
20135
+ lines.push(`${index}. ${accepted} [${q.title}](${q.link}) (${q.score} pts, ${q.answer_count} ans, ${q.view_count} views)`);
20136
+ index++;
20137
+ }
20138
+ return lines.join("\n");
20139
+ }
20140
+ function formatAnswersCompact(answers) {
20141
+ if (answers.length === 0) {
20142
+ return "No answers found.";
20143
+ }
20144
+ const lines = [];
20145
+ let index = 1;
20146
+ for (const a of answers) {
20147
+ const accepted = a.is_accepted ? "[accepted]" : " ";
20148
+ const displayName = a.owner?.display_name ?? "Unknown";
20149
+ lines.push(`${index}. ${accepted} by ${displayName} (${a.score} pts)`);
20150
+ index++;
20151
+ }
20152
+ return lines.join("\n");
20153
+ }
20038
20154
  function formatUsersCompact(users) {
20039
20155
  if (users.length === 0) {
20040
20156
  return "No users found.";
@@ -20318,6 +20434,16 @@ function formatCompact(result) {
20318
20434
  return formatCompactQuestions(result);
20319
20435
  }
20320
20436
  }
20437
+ } else if (typeof result === "object" && result !== null) {
20438
+ if ("question" in result && "answers" in result) {
20439
+ const r = result;
20440
+ let output = formatQuestionsCompact([r.question]);
20441
+ if (r.answers.length > 0) {
20442
+ output += `
20443
+ ${formatAnswersCompact(r.answers)}`;
20444
+ }
20445
+ return output;
20446
+ }
20321
20447
  }
20322
20448
  return JSON.stringify(result);
20323
20449
  }
@@ -20532,11 +20658,13 @@ function watchUrl(videoId) {
20532
20658
  init_retry_utils();
20533
20659
  init_circuit_breaker();
20534
20660
  init_logger();
20661
+ init_log_utils();
20535
20662
  init_metrics();
20536
20663
  init_http_body();
20537
20664
 
20538
20665
  // src/youtube/potoken.ts
20539
20666
  init_logger();
20667
+ init_safe_unref();
20540
20668
  init_metrics();
20541
20669
  var DEFAULT_REQUEST_KEY = "O43z0dpjhgX20SCx4KAo";
20542
20670
  var BRIDGED_GLOBALS = ["window", "document", "location", "navigator", "origin"];
@@ -20549,9 +20677,19 @@ function describeError(error) {
20549
20677
  return String(error);
20550
20678
  }
20551
20679
  var mintChain = Promise.resolve();
20680
+ var MINT_HARD_RELEASE_MS = 5 * 60 * 1e3;
20552
20681
  async function mintPoTokens(identifiers, opts = {}) {
20553
20682
  const run = mintChain.then(() => doMint(identifiers, opts), () => doMint(identifiers, opts));
20554
- mintChain = run.then(() => void 0, () => void 0);
20683
+ const settled = run.then(() => void 0, () => void 0);
20684
+ let hardTimer;
20685
+ const hardRelease = new Promise((resolve9) => {
20686
+ hardTimer = setTimeout(() => {
20687
+ logger.warn("[youtube] potoken mint mutex force-released \u2014 doMint() did not settle within the hard timeout");
20688
+ resolve9();
20689
+ }, MINT_HARD_RELEASE_MS);
20690
+ safeUnref(hardTimer);
20691
+ });
20692
+ mintChain = Promise.race([settled, hardRelease]).finally(() => clearTimeout(hardTimer));
20555
20693
  return run;
20556
20694
  }
20557
20695
  async function doMint(identifiers, opts) {
@@ -20833,7 +20971,7 @@ function parseJson3(body) {
20833
20971
  try {
20834
20972
  parsed = JSON.parse(body);
20835
20973
  } catch {
20836
- logger.debug(`[youtube] timedtext body was not valid json3 (first 120 chars): ${body.slice(0, 120)}`);
20974
+ logger.debug(`[youtube] timedtext body was not valid json3 (first 120 chars): ${redactSecrets(body).slice(0, 120)}`);
20837
20975
  return "";
20838
20976
  }
20839
20977
  const events = parsed.events;
@@ -22362,32 +22500,36 @@ ${result}`);
22362
22500
  if (this.config.KNOWLEDGE_STORE_MODE !== "none") {
22363
22501
  const ksService = await getService(ServiceNames.KNOWLEDGE_STORE, ctx, container);
22364
22502
  if (ksService.isReady()) {
22365
- const writer = await getService(ServiceNames.WRITER_QUEUE, ctx, container);
22366
- const citations = parseCitations(result);
22367
- if (citations.length === 0) {
22368
- logger.warn("[QuickOrchestrator] Researcher produced no parseable CITED LINKS \u2014 no descriptions stored for this session");
22369
- }
22370
- let enqueued = 0;
22371
- for (const cit of citations) {
22372
- if (cit.url) {
22373
- const fullContent = getCachedScrapedContent(this.options.researchId, cit.url);
22374
- const markdown = cit.description || `(source: ${cit.url})`;
22375
- writer.enqueue({
22376
- url: normalizeUrl(cit.url),
22377
- markdown,
22378
- content: fullContent,
22379
- metadata: {
22380
- ingestionType: "synthesis-description",
22381
- source: "researcher",
22382
- synthesizedAt: (/* @__PURE__ */ new Date()).toISOString(),
22383
- description: cit.description || "",
22384
- fullContentSnippet: fullContent?.substring(0, 5e3)
22385
- }
22386
- });
22387
- enqueued++;
22503
+ const writer = await ksService.getWriterQueue();
22504
+ if (!writer) {
22505
+ logger.debug("[QuickOrchestrator] Writer queue unavailable, skipping link descriptions");
22506
+ } else {
22507
+ const citations = parseCitations(result);
22508
+ if (citations.length === 0) {
22509
+ logger.warn("[QuickOrchestrator] Researcher produced no parseable CITED LINKS \u2014 no descriptions stored for this session");
22388
22510
  }
22511
+ let enqueued = 0;
22512
+ for (const cit of citations) {
22513
+ if (cit.url) {
22514
+ const fullContent = getCachedScrapedContent(this.options.researchId, cit.url);
22515
+ const markdown = cit.description || `(source: ${cit.url})`;
22516
+ writer.enqueue({
22517
+ url: normalizeUrl(cit.url),
22518
+ markdown,
22519
+ content: fullContent,
22520
+ metadata: {
22521
+ ingestionType: "synthesis-description",
22522
+ source: "researcher",
22523
+ synthesizedAt: (/* @__PURE__ */ new Date()).toISOString(),
22524
+ description: cit.description || "",
22525
+ fullContentSnippet: fullContent?.substring(0, 5e3)
22526
+ }
22527
+ });
22528
+ enqueued++;
22529
+ }
22530
+ }
22531
+ if (enqueued > 0) await writer.drain();
22389
22532
  }
22390
- if (enqueued > 0) await writer.drain();
22391
22533
  }
22392
22534
  }
22393
22535
  } catch (err) {
@@ -23180,7 +23322,11 @@ var ResearchOrchestrationService = class {
23180
23322
  return;
23181
23323
  }
23182
23324
  const synthesisService = await getService(ServiceNames.RESEARCH_SYNTHESIS_SERVICE, ctx, container);
23183
- const writer = await getService(ServiceNames.WRITER_QUEUE, ctx, container);
23325
+ const writer = await ksService.getWriterQueue();
23326
+ if (!writer) {
23327
+ logger.debug("[ResearchOrchestrationService] Writer queue unavailable, skipping link descriptions");
23328
+ return;
23329
+ }
23184
23330
  const roundPrefix = `${round}.`;
23185
23331
  let enqueued = 0;
23186
23332
  let researcherCount = 0;
@@ -23833,6 +23979,7 @@ async function initializeOrchestrationServices(ctx, container = getServiceContai
23833
23979
  init_service_registry();
23834
23980
  init_service_interfaces();
23835
23981
  init_logger();
23982
+ init_log_utils();
23836
23983
  init_config();
23837
23984
  init_metrics();
23838
23985
 
@@ -24175,7 +24322,7 @@ function getLastResearcherOutcome() {
24175
24322
  }
24176
24323
  function logRunErrorSummary(report, depthLabel, status) {
24177
24324
  if (!report || report.totalErrors === 0) return;
24178
- const top = report.patterns.slice(0, 3).map((p) => `"${String(p.message ?? p.signature ?? "error").slice(0, 48)}" \xD7${p.count}`).join(", ");
24325
+ const top = report.patterns.slice(0, 3).map((p) => `"${redactSecrets(String(p.message ?? p.signature ?? "error")).slice(0, 48)}" \xD7${p.count}`).join(", ");
24179
24326
  logger.warn(`[SDK] run ${status} with ${report.totalErrors} tracked error(s) across ${report.uniquePatterns} pattern(s) [${depthLabel}]${top ? ` \u2014 top: ${top}` : ""}`);
24180
24327
  }
24181
24328
  function wrapObserverForOutcomeCapture(observer, onPlanned) {