@lincoln504/pi-research 0.1.12 → 1.0.0

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 (248) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +64 -115
  3. package/dist/cli.mjs +23211 -0
  4. package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
  5. package/dist/prompts/research-knowledge-search-extractor.md +41 -0
  6. package/dist/prompts/research-tool-usage.md +92 -0
  7. package/dist/prompts/researcher.md +89 -0
  8. package/dist/prompts/system-coordinator.md +75 -0
  9. package/dist/prompts/system-lead-evaluator.md +118 -0
  10. package/dist/thread-worker.mjs +2611 -0
  11. package/dist/webgpu-probe.mjs +80 -0
  12. package/docs/AGENT-SKILL.md +102 -0
  13. package/docs/ARCHITECTURE.md +306 -80
  14. package/docs/CONFIGURATION.md +249 -0
  15. package/docs/KNOWLEDGE-STORE.md +213 -0
  16. package/docs/PI-EXTENSION.md +91 -0
  17. package/docs/SDK.md +113 -0
  18. package/docs/THIRD-PARTY-NOTICES.md +64 -0
  19. package/package.json +102 -36
  20. package/scripts/build.cjs +140 -0
  21. package/scripts/cleanup.cjs +149 -0
  22. package/scripts/prepare.cjs +28 -0
  23. package/scripts/setup.cjs +134 -0
  24. package/skills/pi-research/README.md +90 -0
  25. package/skills/pi-research/SKILL.md +102 -0
  26. package/skills/pi-research/references/configuration.md +77 -0
  27. package/skills/pi-research/scripts/run.mjs +237 -0
  28. package/src/cleanup/research-cleanup.ts +140 -0
  29. package/src/cli.ts +1044 -0
  30. package/src/config.ts +1036 -147
  31. package/src/constants.ts +115 -84
  32. package/src/core/health-check-service.ts +265 -0
  33. package/src/core/interfaces/health-check-interfaces.ts +50 -0
  34. package/src/core/interfaces/knowledge-interfaces.ts +190 -0
  35. package/src/core/interfaces/observer-interfaces.ts +67 -0
  36. package/src/core/interfaces/orchestration-interfaces.ts +86 -0
  37. package/src/core/interfaces/planning-interfaces.ts +100 -0
  38. package/src/core/interfaces/process-interfaces.ts +37 -0
  39. package/src/core/interfaces/research-plan-types.ts +52 -0
  40. package/src/core/interfaces/scheduler-interfaces.ts +91 -0
  41. package/src/core/interfaces/service-names.ts +39 -0
  42. package/src/core/interfaces/state-manager-interfaces.ts +29 -0
  43. package/src/core/interfaces/steering-interfaces.ts +31 -0
  44. package/src/core/llm/agentic-repair.ts +169 -0
  45. package/src/core/llm/inject-date.ts +52 -0
  46. package/src/core/llm/llm-timeout.ts +64 -0
  47. package/src/core/llm/llm-utils.ts +86 -0
  48. package/src/core/llm/model-registry-factory.ts +287 -0
  49. package/src/core/llm/pi-ai-completion.ts +29 -0
  50. package/src/core/llm/prompts.ts +50 -0
  51. package/src/core/llm/research-model-resolver.ts +91 -0
  52. package/src/core/planning-constants.ts +13 -0
  53. package/src/core/planning-service.ts +649 -0
  54. package/src/core/planning-utils.ts +299 -0
  55. package/src/core/scheduler-factory.ts +48 -0
  56. package/src/core/scheduler-service.ts +123 -0
  57. package/src/core/service-initialization.ts +194 -0
  58. package/src/core/service-interfaces.ts +48 -0
  59. package/src/core/service-registry.ts +659 -0
  60. package/src/healthcheck/index.ts +271 -265
  61. package/src/healthcheck/registry.ts +130 -0
  62. package/src/index.ts +639 -50
  63. package/src/infrastructure/browser/browser-cleanup.ts +309 -0
  64. package/src/infrastructure/browser/browser-client.ts +199 -0
  65. package/src/infrastructure/browser/browser-error-utils.ts +160 -0
  66. package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
  67. package/src/infrastructure/browser/browser-server.ts +174 -0
  68. package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
  69. package/src/infrastructure/browser/cleanup-utils.ts +155 -0
  70. package/src/infrastructure/browser/client-agent.ts +31 -0
  71. package/src/infrastructure/browser/config.ts +288 -0
  72. package/src/infrastructure/browser/ensure-browser.ts +205 -0
  73. package/src/infrastructure/browser/index.ts +37 -0
  74. package/src/infrastructure/browser/priority-task-queue.ts +226 -0
  75. package/src/infrastructure/browser/scheduler-factory.ts +354 -0
  76. package/src/infrastructure/browser/task-execution-service.ts +209 -0
  77. package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
  78. package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
  79. package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
  80. package/src/infrastructure/browser/thread-worker-types.ts +25 -0
  81. package/src/infrastructure/browser/thread-worker.mjs +2611 -0
  82. package/src/infrastructure/browser/thread-worker.ts +168 -0
  83. package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
  84. package/src/infrastructure/embedding/embedding-client.ts +205 -0
  85. package/src/infrastructure/embedding/embedding-factory.ts +324 -0
  86. package/src/infrastructure/embedding/embedding-server.ts +535 -0
  87. package/src/infrastructure/file-lock-service.ts +592 -0
  88. package/src/infrastructure/gpu-resource-service.ts +191 -0
  89. package/src/infrastructure/knowledge-store-service.ts +475 -0
  90. package/src/infrastructure/metrics-service.ts +129 -0
  91. package/src/infrastructure/process-lifecycle-service.ts +253 -0
  92. package/src/infrastructure/scheduler-factory-service.ts +58 -0
  93. package/src/infrastructure/service-initialization.ts +292 -0
  94. package/src/infrastructure/state/state-backup-manager.ts +290 -0
  95. package/src/infrastructure/state/state-browser-api.ts +62 -0
  96. package/src/infrastructure/state/state-browser-manager.ts +70 -0
  97. package/src/infrastructure/state/state-manager-service.ts +264 -0
  98. package/src/infrastructure/state/state-manager.ts +547 -0
  99. package/src/infrastructure/state/state-metrics.ts +92 -0
  100. package/src/infrastructure/state/state-migration.ts +103 -0
  101. package/src/infrastructure/state/state-path-configuration.ts +119 -0
  102. package/src/infrastructure/state/state-session-api.ts +94 -0
  103. package/src/infrastructure/state/state-session-manager.ts +127 -0
  104. package/src/infrastructure/state/state-validator.ts +93 -0
  105. package/src/infrastructure/types/state-types.ts +85 -0
  106. package/src/knowledge/chunker.ts +157 -0
  107. package/src/knowledge/embedder-init.ts +413 -0
  108. package/src/knowledge/embedder-utils.ts +296 -0
  109. package/src/knowledge/embedder.ts +592 -0
  110. package/src/knowledge/index.ts +126 -0
  111. package/src/knowledge/lancedb-loader.ts +35 -0
  112. package/src/knowledge/migration.ts +17 -0
  113. package/src/knowledge/model-config.ts +132 -0
  114. package/src/knowledge/onnx-env.ts +48 -0
  115. package/src/knowledge/store-operations.ts +291 -0
  116. package/src/knowledge/store-schema.ts +79 -0
  117. package/src/knowledge/store.ts +1251 -0
  118. package/src/knowledge/transformers-loader.ts +31 -0
  119. package/src/knowledge/webgpu-probe.mjs +80 -0
  120. package/src/knowledge/webgpu-viability.ts +255 -0
  121. package/src/knowledge/writer-queue.ts +252 -0
  122. package/src/logger.ts +316 -122
  123. package/src/observers/research-observer-impl.ts +459 -0
  124. package/src/orchestration/deep-research-orchestrator.ts +524 -773
  125. package/src/orchestration/deep-research-types.ts +47 -35
  126. package/src/orchestration/headless-observer.ts +114 -0
  127. package/src/orchestration/orchestration-types.ts +33 -0
  128. package/src/orchestration/quick-research-orchestrator.ts +463 -0
  129. package/src/orchestration/research-orchestration-service.ts +549 -0
  130. package/src/orchestration/research-session-service.ts +178 -0
  131. package/src/orchestration/research-synthesis-service.ts +348 -0
  132. package/src/orchestration/researcher-executor.ts +394 -0
  133. package/src/orchestration/researcher.ts +115 -42
  134. package/src/orchestration/service-initialization.ts +109 -0
  135. package/src/orchestration/session-context.ts +7 -3
  136. package/src/orchestration/session-state.ts +672 -0
  137. package/src/prompts/research-knowledge-relevance-triage.md +35 -0
  138. package/src/prompts/research-knowledge-search-extractor.md +41 -0
  139. package/src/prompts/research-tool-usage.md +63 -16
  140. package/src/prompts/researcher.md +59 -136
  141. package/src/prompts/system-coordinator.md +64 -13
  142. package/src/prompts/system-lead-evaluator.md +82 -189
  143. package/src/research-config.ts +872 -0
  144. package/src/sdk.ts +847 -0
  145. package/src/security/cisa-kev.ts +72 -9
  146. package/src/security/cvss.ts +92 -0
  147. package/src/security/ecosystem.ts +45 -0
  148. package/src/security/github-advisories.ts +168 -428
  149. package/src/security/github-advisory-types.ts +282 -0
  150. package/src/security/index.ts +63 -177
  151. package/src/security/nvd-client.ts +550 -0
  152. package/src/security/nvd-types.ts +179 -0
  153. package/src/security/osv-client.ts +246 -0
  154. package/src/security/osv-types.ts +285 -0
  155. package/src/security/types.ts +67 -53
  156. package/src/skill-install/skill-installer.ts +519 -0
  157. package/src/stackexchange/index.ts +81 -27
  158. package/src/stackexchange/output/compact.ts +7 -6
  159. package/src/stackexchange/output/table.ts +11 -7
  160. package/src/stackexchange/queries.ts +0 -10
  161. package/src/stackexchange/rest-client.ts +141 -66
  162. package/src/stackexchange/types.ts +0 -17
  163. package/src/tool.ts +15 -423
  164. package/src/tools/grep.ts +32 -310
  165. package/src/tools/health-tool-definition.ts +101 -0
  166. package/src/tools/index.ts +38 -9
  167. package/src/tools/research-knowledge-search.ts +974 -0
  168. package/src/tools/research-knowledge-types.ts +77 -0
  169. package/src/tools/research-tool-definition.ts +494 -0
  170. package/src/tools/scrape.ts +286 -388
  171. package/src/tools/search.ts +98 -153
  172. package/src/tools/security.ts +107 -80
  173. package/src/tools/stackexchange.ts +58 -35
  174. package/src/tools/youtube-transcript.ts +163 -0
  175. package/src/tui/knowledge-search-panel.ts +86 -0
  176. package/src/tui/pulse.ts +86 -0
  177. package/src/tui/research-health.ts +124 -0
  178. package/src/tui/research-panel-color-utils.ts +340 -0
  179. package/src/tui/research-panel-format.ts +54 -0
  180. package/src/tui/research-panel-state.ts +404 -0
  181. package/src/tui/research-panel-wave.ts +280 -0
  182. package/src/tui/research-panel.ts +339 -435
  183. package/src/tui/research-tui-manager.ts +233 -0
  184. package/src/tui/tui-controller.ts +137 -0
  185. package/src/types/extension-context.ts +18 -49
  186. package/src/types/index.ts +61 -0
  187. package/src/types/llm.ts +68 -29
  188. package/src/types/research-panel-types.ts +61 -0
  189. package/src/utils/circuit-breaker.ts +146 -0
  190. package/src/utils/citation-utils.ts +97 -0
  191. package/src/utils/disk-space-checker.ts +55 -0
  192. package/src/utils/error-tracker.ts +265 -0
  193. package/src/utils/host-config.ts +45 -0
  194. package/src/utils/input-validation.ts +8 -10
  195. package/src/utils/json-utils.ts +150 -16
  196. package/src/utils/llm-usage.ts +54 -0
  197. package/src/utils/log-rotation.ts +125 -0
  198. package/src/utils/log-utils.ts +159 -0
  199. package/src/utils/make-resource-loader.ts +3 -1
  200. package/src/utils/metrics-summary.ts +459 -0
  201. package/src/utils/metrics.ts +302 -0
  202. package/src/utils/research-export.ts +28 -12
  203. package/src/utils/safe-unref.ts +31 -0
  204. package/src/utils/shared-links.ts +195 -90
  205. package/src/utils/shutdown-manager.ts +65 -0
  206. package/src/utils/stdio-capture.ts +407 -0
  207. package/src/utils/text-utils.ts +264 -7
  208. package/src/utils/tool-usage-tracker.ts +83 -16
  209. package/src/utils/url-utils.ts +255 -0
  210. package/src/utils/user-agent.ts +28 -0
  211. package/src/web-research/browser-search.ts +179 -0
  212. package/src/web-research/retry-utils.ts +173 -77
  213. package/src/web-research/scraper-types.ts +72 -0
  214. package/src/web-research/scraper-utils.ts +434 -0
  215. package/src/web-research/search.ts +78 -218
  216. package/src/web-research/types.ts +9 -24
  217. package/src/web-research/utils.ts +2 -143
  218. package/src/web-research/web-scraper.ts +477 -0
  219. package/src/youtube/index.ts +154 -0
  220. package/src/youtube/potoken.ts +259 -0
  221. package/src/youtube/transcript-client.ts +390 -0
  222. package/src/youtube/video-id.ts +118 -0
  223. package/config/searxng/default-settings.yml +0 -189
  224. package/config/searxng/limiter.toml +0 -34
  225. package/config/tooling/eslint.config.js +0 -73
  226. package/config/tooling/vitest.config.integration.ts +0 -17
  227. package/config/tooling/vitest.config.ts +0 -40
  228. package/config/tooling/vitest.config.unit.ts +0 -12
  229. package/docs/assets/README-banner.jpg +0 -0
  230. package/scripts/setup.js +0 -174
  231. package/src/infrastructure/searxng-lifecycle.ts +0 -577
  232. package/src/infrastructure/searxng-manager.ts +0 -1183
  233. package/src/infrastructure/state-manager.ts +0 -918
  234. package/src/orchestration/deep-research-reducer.ts +0 -129
  235. package/src/orchestration/id-utils.ts +0 -63
  236. package/src/security/nvd.ts +0 -532
  237. package/src/security/osv.ts +0 -487
  238. package/src/stackexchange/cache.ts +0 -111
  239. package/src/stackexchange/output/index.ts +0 -7
  240. package/src/stackexchange/output/json.ts +0 -21
  241. package/src/tui/searxng-status.ts +0 -80
  242. package/src/utils/inject-date.ts +0 -29
  243. package/src/utils/research-error.ts +0 -205
  244. package/src/utils/searxng-config.ts +0 -94
  245. package/src/utils/session-state.ts +0 -295
  246. package/src/web-research/dependencies.ts +0 -20
  247. package/src/web-research/scrapers.ts +0 -935
  248. package/tsconfig.json +0 -40
@@ -0,0 +1,2611 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+
16
+ // src/utils/log-utils.ts
17
+ import { AsyncLocalStorage } from "node:async_hooks";
18
+ import * as os from "node:os";
19
+ import * as path from "node:path";
20
+ function buildDefaultDebugLogPath() {
21
+ const override = process.env["PI_RESEARCH_LOG_PATH"];
22
+ if (override) return override;
23
+ return path.join(os.tmpdir(), "pi-research.log");
24
+ }
25
+ function isVerboseFromEnv() {
26
+ return process.env["PI_RESEARCH_DEBUG"] === "true";
27
+ }
28
+ function getLogContext() {
29
+ return logContextStorage.getStore() ?? {};
30
+ }
31
+ function safeJsonStringify(value) {
32
+ try {
33
+ return JSON.stringify(value, null, 0);
34
+ } catch {
35
+ return "[unserializable]";
36
+ }
37
+ }
38
+ function formatArg(arg) {
39
+ if (arg instanceof Error) {
40
+ return arg.stack ?? arg.message;
41
+ }
42
+ if (typeof arg === "object" && arg !== null) {
43
+ return safeJsonStringify(arg);
44
+ }
45
+ return String(arg);
46
+ }
47
+ function redactSecrets(message) {
48
+ let out = message.length > MAX_LOG_MESSAGE_LENGTH ? `${message.slice(0, MAX_LOG_MESSAGE_LENGTH)}\u2026[truncated ${message.length - MAX_LOG_MESSAGE_LENGTH} chars]` : message;
49
+ out = out.replace(ANSI_PATTERN, "");
50
+ out = out.replace(URL_CREDENTIALS_PATTERN, "$1[REDACTED]@");
51
+ out = out.replace(JWT_PATTERN, "[REDACTED]");
52
+ out = out.replace(BASIC_AUTH_PATTERN, "Basic [REDACTED]");
53
+ out = out.replace(BEARER_AUTH_PATTERN, "Bearer [REDACTED]");
54
+ out = out.replace(SENSITIVE_KV_PATTERN, (_m, key, sep) => `${key}${sep}[REDACTED]`);
55
+ out = out.replace(KNOWN_TOKEN_PATTERN, "[REDACTED]");
56
+ out = out.replace(PROVIDER_TOKEN_PATTERN, "[REDACTED]");
57
+ out = out.replace(LONG_HEX_SECRET_PATTERN, "[REDACTED]");
58
+ return out;
59
+ }
60
+ var logContextStorage, MAX_LOG_MESSAGE_LENGTH, ANSI_PATTERN, URL_CREDENTIALS_PATTERN, SENSITIVE_KV_PATTERN, KNOWN_TOKEN_PATTERN, JWT_PATTERN, BASIC_AUTH_PATTERN, BEARER_AUTH_PATTERN, PROVIDER_TOKEN_PATTERN, LONG_HEX_SECRET_PATTERN;
61
+ var init_log_utils = __esm({
62
+ "src/utils/log-utils.ts"() {
63
+ "use strict";
64
+ logContextStorage = new AsyncLocalStorage();
65
+ MAX_LOG_MESSAGE_LENGTH = 1e4;
66
+ ANSI_PATTERN = /\x1b\[[0-9;]*[A-Za-z]/g;
67
+ URL_CREDENTIALS_PATTERN = /([a-z][a-z0-9+.-]*:\/\/)[^/\s:@]+:[^/\s:@]+@/gi;
68
+ 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;
69
+ 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;
70
+ JWT_PATTERN = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g;
71
+ BASIC_AUTH_PATTERN = /\bBasic\s+[A-Za-z0-9+/]{16,}={0,2}/gi;
72
+ BEARER_AUTH_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/-]+=*/gi;
73
+ 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;
74
+ LONG_HEX_SECRET_PATTERN = /\b[0-9a-fA-F]{32,}\b/g;
75
+ }
76
+ });
77
+
78
+ // src/utils/error-tracker.ts
79
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
80
+ var trackerStorage, ErrorTracker, globalInstance, errorTracker;
81
+ var init_error_tracker = __esm({
82
+ "src/utils/error-tracker.ts"() {
83
+ "use strict";
84
+ trackerStorage = new AsyncLocalStorage2();
85
+ ErrorTracker = class {
86
+ patterns = /* @__PURE__ */ new Map();
87
+ MAX_CONTEXTS_PER_PATTERN = 10;
88
+ // Bound total memory. The global singleton is never clear()'d, so out-of-run errors (logger,
89
+ // scrapers, healthcheck) would otherwise grow these maps without limit over a long-lived host.
90
+ MAX_PATTERNS = 500;
91
+ MAX_DOMAINS_PER_PATTERN = 100;
92
+ /**
93
+ * Normalize an error message to extract a stable signature.
94
+ * Removes UUIDs, numbers (except HTTP status codes), URLs, and normalizes whitespace.
95
+ *
96
+ * @param message - The error message to normalize
97
+ * @returns A normalized signature for grouping similar errors
98
+ */
99
+ extractSignature(message) {
100
+ return message.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "<UUID>").replace(/\b(?![12]\d{2}\b|[3-5]\d{2}\b)\d+\b/g, "<NUM>").replace(/https?:\/\/[^\s]+/g, "<URL>").replace(/\s+/g, " ").trim();
101
+ }
102
+ /**
103
+ * Track an error with optional context.
104
+ * Groups similar errors by normalized signature and maintains a rolling buffer of contexts.
105
+ *
106
+ * @param error - The error to track (Error object, string message, or unknown)
107
+ * @param context - Optional context information about where the error occurred
108
+ */
109
+ trackError(error, context2 = {}) {
110
+ let message;
111
+ if (error instanceof Error) {
112
+ message = error.message;
113
+ } else if (typeof error === "string") {
114
+ message = error;
115
+ } else {
116
+ message = String(error);
117
+ }
118
+ const signature = this.extractSignature(message);
119
+ const now = (/* @__PURE__ */ new Date()).toISOString();
120
+ let pattern = this.patterns.get(signature);
121
+ if (!pattern) {
122
+ if (this.patterns.size >= this.MAX_PATTERNS) {
123
+ this.evictLeastFrequentPattern();
124
+ }
125
+ pattern = {
126
+ signature,
127
+ message,
128
+ count: 0,
129
+ firstSeen: now,
130
+ lastSeen: now,
131
+ contexts: [],
132
+ domainCounts: /* @__PURE__ */ new Map()
133
+ };
134
+ this.patterns.set(signature, pattern);
135
+ }
136
+ pattern.count++;
137
+ pattern.lastSeen = now;
138
+ if (context2.domain) {
139
+ const existing = pattern.domainCounts.get(context2.domain);
140
+ if (existing !== void 0) {
141
+ pattern.domainCounts.set(context2.domain, existing + 1);
142
+ } else if (pattern.domainCounts.size < this.MAX_DOMAINS_PER_PATTERN) {
143
+ pattern.domainCounts.set(context2.domain, 1);
144
+ }
145
+ }
146
+ pattern.contexts.push(context2);
147
+ if (pattern.contexts.length > this.MAX_CONTEXTS_PER_PATTERN) {
148
+ pattern.contexts.shift();
149
+ }
150
+ }
151
+ /**
152
+ * Generate a complete error report with sorted patterns and domain/type breakdowns.
153
+ *
154
+ * @returns An ErrorReport with aggregated error information
155
+ */
156
+ getReport() {
157
+ const patterns = Array.from(this.patterns.values());
158
+ let totalErrors = 0;
159
+ for (const pattern of patterns) {
160
+ totalErrors += pattern.count;
161
+ }
162
+ patterns.sort((a, b) => b.count - a.count);
163
+ const byDomain = this.getErrorsByDomain();
164
+ const byType = this.getErrorsByType();
165
+ return {
166
+ totalErrors,
167
+ uniquePatterns: patterns.length,
168
+ patterns,
169
+ byDomain,
170
+ byType
171
+ };
172
+ }
173
+ /**
174
+ * Group errors by domain from context information.
175
+ *
176
+ * @returns A Map of domain names to error counts
177
+ */
178
+ getErrorsByDomain() {
179
+ const totalDomainCounts = /* @__PURE__ */ new Map();
180
+ for (const pattern of this.patterns.values()) {
181
+ for (const [domain, count] of pattern.domainCounts.entries()) {
182
+ totalDomainCounts.set(domain, (totalDomainCounts.get(domain) ?? 0) + count);
183
+ }
184
+ }
185
+ return totalDomainCounts;
186
+ }
187
+ /**
188
+ * Group errors by type (first part of signature before colon or first few words).
189
+ *
190
+ * @returns A Map of error types to error counts
191
+ */
192
+ getErrorsByType() {
193
+ const typeCounts = /* @__PURE__ */ new Map();
194
+ for (const pattern of this.patterns.values()) {
195
+ const parts = pattern.signature.split(":");
196
+ let type = parts[0]?.trim() ?? "Unknown";
197
+ if (type === pattern.signature) {
198
+ const words = pattern.signature.split(" ");
199
+ const firstWords = words.slice(0, 3).filter((w) => w.length > 0);
200
+ type = firstWords.length > 0 ? firstWords.join(" ") : "Unknown";
201
+ }
202
+ typeCounts.set(type, (typeCounts.get(type) ?? 0) + pattern.count);
203
+ }
204
+ return typeCounts;
205
+ }
206
+ /**
207
+ * Evict the least-frequent pattern to keep the map bounded (see MAX_PATTERNS). O(n) but only
208
+ * runs when a new signature is added while already at the cap.
209
+ */
210
+ evictLeastFrequentPattern() {
211
+ let victimKey;
212
+ let victimCount = Infinity;
213
+ for (const [key, p] of this.patterns) {
214
+ if (p.count < victimCount) {
215
+ victimCount = p.count;
216
+ victimKey = key;
217
+ }
218
+ }
219
+ if (victimKey !== void 0) {
220
+ this.patterns.delete(victimKey);
221
+ }
222
+ }
223
+ /**
224
+ * Clear all tracked errors (called between research runs).
225
+ */
226
+ clear() {
227
+ this.patterns.clear();
228
+ }
229
+ };
230
+ globalInstance = new ErrorTracker();
231
+ errorTracker = new Proxy(globalInstance, {
232
+ get(target, prop, receiver) {
233
+ const currentTracker = trackerStorage.getStore() ?? target;
234
+ const value = Reflect.get(currentTracker, prop, receiver);
235
+ if (typeof value === "function") {
236
+ return value.bind(currentTracker);
237
+ }
238
+ return value;
239
+ }
240
+ });
241
+ }
242
+ });
243
+
244
+ // src/utils/log-rotation.ts
245
+ import * as fs2 from "node:fs";
246
+ import * as path2 from "node:path";
247
+ var LogRotation;
248
+ var init_log_rotation = __esm({
249
+ "src/utils/log-rotation.ts"() {
250
+ "use strict";
251
+ LogRotation = class {
252
+ MAX_LOG_SIZE = 10 * 1024 * 1024;
253
+ // 10MB max file size
254
+ MAX_LOG_FILES = 10;
255
+ // Keep last 10 archived logs
256
+ lastRotationCheck = 0;
257
+ logger;
258
+ constructor(logger2) {
259
+ this.logger = logger2;
260
+ }
261
+ /**
262
+ * Clear all research logs including archives.
263
+ */
264
+ clearLogs(logFile, logDir) {
265
+ try {
266
+ if (fs2.existsSync(logFile)) {
267
+ fs2.unlinkSync(logFile);
268
+ }
269
+ const files = fs2.readdirSync(logDir);
270
+ const baseName = path2.basename(logFile);
271
+ const archives = files.filter((f) => f.startsWith(baseName) && f !== baseName);
272
+ for (const archive of archives) {
273
+ try {
274
+ fs2.unlinkSync(path2.join(logDir, archive));
275
+ } catch {
276
+ }
277
+ }
278
+ this.logger.log("[Logger] All logs and archives cleared.");
279
+ } catch (err) {
280
+ this.logger.log("[Logger] Failed to clear logs:", err);
281
+ }
282
+ }
283
+ /**
284
+ * Rotate log files when they exceed MAX_LOG_SIZE.
285
+ * Archives are created with ISO timestamp suffix.
286
+ * Old archives beyond MAX_LOG_FILES are cleaned up.
287
+ */
288
+ rotateLogFile(logFile, logDir) {
289
+ try {
290
+ const stats = fs2.statSync(logFile);
291
+ const fileSize = stats.size;
292
+ if (fileSize <= this.MAX_LOG_SIZE) {
293
+ return;
294
+ }
295
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
296
+ const archivePath = `${logFile}.${timestamp}`;
297
+ try {
298
+ fs2.renameSync(logFile, archivePath);
299
+ } catch (renameErr) {
300
+ if (process.platform === "win32") {
301
+ fs2.copyFileSync(logFile, archivePath);
302
+ fs2.unlinkSync(logFile);
303
+ } else {
304
+ throw renameErr;
305
+ }
306
+ }
307
+ try {
308
+ const files = fs2.readdirSync(logDir);
309
+ const logFiles = files.filter((f) => f.startsWith(path2.basename(logFile)) && f !== path2.basename(logFile)).sort();
310
+ const toDelete = logFiles.slice(0, -this.MAX_LOG_FILES);
311
+ for (const file of toDelete) {
312
+ try {
313
+ fs2.unlinkSync(path2.join(logDir, file));
314
+ } catch {
315
+ }
316
+ }
317
+ } catch {
318
+ }
319
+ this.logger.log("[Logger] Rotated log file to:", archivePath);
320
+ } catch (_error) {
321
+ }
322
+ }
323
+ /**
324
+ * Check if rotation is needed and perform if necessary.
325
+ *
326
+ * Throttled to one filesystem stat per 60s. We deliberately do NOT offer a
327
+ * `force` bypass: a previous version forced a check on every WARN/ERROR, which
328
+ * turned into a synchronous fs.statSync per line (thousands per heavy run) and
329
+ * also reset the throttle so the timer path never fired. The 60s timer
330
+ * preserves all log data on rotation regardless of when it lands — a file may
331
+ * transiently exceed MAX_LOG_SIZE by at most one check interval, which is fine.
332
+ * @returns true if a rotation check was performed
333
+ */
334
+ checkAndRotate(logFile, logDir) {
335
+ const now = Date.now();
336
+ if (now - this.lastRotationCheck > 6e4) {
337
+ this.rotateLogFile(logFile, logDir);
338
+ this.lastRotationCheck = now;
339
+ return true;
340
+ }
341
+ return false;
342
+ }
343
+ };
344
+ }
345
+ });
346
+
347
+ // src/utils/disk-space-checker.ts
348
+ import * as fs3 from "node:fs";
349
+ var DiskSpaceChecker;
350
+ var init_disk_space_checker = __esm({
351
+ "src/utils/disk-space-checker.ts"() {
352
+ "use strict";
353
+ DiskSpaceChecker = class {
354
+ MIN_DISK_SPACE_BYTES = 1048576;
355
+ // 1MB minimum
356
+ DISK_SPACE_CHECK_INTERVAL_MS = 6e4;
357
+ // Check every 60 seconds
358
+ lastDiskSpaceCheck = 0;
359
+ hasDiskSpace = true;
360
+ /**
361
+ * Check if there is sufficient disk space for logging.
362
+ * Checks are throttled to avoid excessive filesystem operations.
363
+ */
364
+ checkDiskSpace(logDir) {
365
+ const now = Date.now();
366
+ if (now - this.lastDiskSpaceCheck < this.DISK_SPACE_CHECK_INTERVAL_MS) {
367
+ return this.hasDiskSpace;
368
+ }
369
+ this.lastDiskSpaceCheck = now;
370
+ try {
371
+ const stats = fs3.statfsSync(logDir);
372
+ const availableBytes = stats.bavail * stats.bsize;
373
+ if (availableBytes < this.MIN_DISK_SPACE_BYTES) {
374
+ this.hasDiskSpace = false;
375
+ process.stderr.write(
376
+ `[pi-research] Insufficient disk space for logging: ${Math.round(availableBytes / 1024 / 1024)}MB available, minimum ${this.MIN_DISK_SPACE_BYTES / 1024 / 1024}MB required
377
+ `
378
+ );
379
+ } else {
380
+ this.hasDiskSpace = true;
381
+ }
382
+ } catch (_error) {
383
+ this.hasDiskSpace = true;
384
+ }
385
+ return this.hasDiskSpace;
386
+ }
387
+ };
388
+ }
389
+ });
390
+
391
+ // src/utils/stdio-capture.ts
392
+ import { createRequire } from "node:module";
393
+ import { TextDecoder } from "node:util";
394
+ function recomputeGlobalCapturing() {
395
+ isAnyLoggerCapturingOutput = unkeyedCaptureDepth > 0 || Array.from(sessionCaptureStates.values()).some((v) => v);
396
+ }
397
+ function isSessionCapturing(sessionId) {
398
+ if (sessionId) {
399
+ return sessionCaptureStates.get(sessionId) ?? false;
400
+ }
401
+ return isAnyLoggerCapturingOutput;
402
+ }
403
+ function setSessionCapturing(sessionId, capturing) {
404
+ sessionCaptureStates.set(sessionId, capturing);
405
+ if (capturing) {
406
+ isAnyLoggerCapturingOutput = true;
407
+ } else {
408
+ recomputeGlobalCapturing();
409
+ }
410
+ }
411
+ function isComplexTui(message) {
412
+ if (!message.includes("\x1B[")) {
413
+ return false;
414
+ }
415
+ for (const pattern of COMPLEX_TUI_PATTERNS) {
416
+ if (message.includes(pattern)) {
417
+ return true;
418
+ }
419
+ }
420
+ if (/\u001b\[\d+;\d+H/.test(message) || /\u001b\[\d+[ABCD]/.test(message)) {
421
+ return true;
422
+ }
423
+ return false;
424
+ }
425
+ function isNativeLog(message) {
426
+ return NATIVE_LOG_PATTERNS.some((pattern) => message.includes(pattern));
427
+ }
428
+ function isBoxDrawing(message) {
429
+ return BOX_DRAWING_CHARS.test(message);
430
+ }
431
+ function shouldRedirectStderr(stat) {
432
+ return !stat.isFIFO() && !stat.isSocket();
433
+ }
434
+ function createConsolePatch(level, logFile, hasSufficientDiskSpace) {
435
+ return (...args) => {
436
+ if (!hasSufficientDiskSpace()) {
437
+ return;
438
+ }
439
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
440
+ const entry = {
441
+ timestamp,
442
+ level: `CONSOLE_${level.toUpperCase()}`,
443
+ ...getLogContext(),
444
+ // Redact secrets + bound size, matching Logger.emit(); captured console
445
+ // output can echo auth headers / tokens from dependencies.
446
+ message: redactSecrets(args.map(formatArg).join(" "))
447
+ };
448
+ try {
449
+ fs4.appendFileSync(logFile, `${safeJsonStringify(entry)}
450
+ `);
451
+ } catch {
452
+ }
453
+ };
454
+ }
455
+ async function captureStdio(logFile, hasSufficientDiskSpace, task, sessionId) {
456
+ const sessionCapturing = sessionId ? isSessionCapturing(sessionId) : false;
457
+ if (!logFile || sessionCapturing || !sessionId && isAnyLoggerCapturingOutput) {
458
+ return await task();
459
+ }
460
+ if (sessionId) {
461
+ setSessionCapturing(sessionId, true);
462
+ } else {
463
+ unkeyedCaptureDepth++;
464
+ isAnyLoggerCapturingOutput = true;
465
+ }
466
+ const decoder = new TextDecoder();
467
+ const originalStderrWrite = process.stderr.write;
468
+ const originalStdoutWrite = process.stdout.write;
469
+ const originalFsWriteSync = fs4.writeSync;
470
+ const originalConsole = {
471
+ log: console.log,
472
+ info: console.info,
473
+ warn: console.warn,
474
+ error: console.error,
475
+ debug: console.debug
476
+ };
477
+ let savedFd2 = -1;
478
+ let fd2Redirected = false;
479
+ if (process.platform !== "win32") {
480
+ try {
481
+ const stat = fs4.fstatSync(2);
482
+ if (shouldRedirectStderr(stat)) {
483
+ savedFd2 = fs4.openSync("/dev/fd/2", "a");
484
+ fs4.closeSync(2);
485
+ const newFd = fs4.openSync(logFile, "a");
486
+ if (newFd === 2) {
487
+ fd2Redirected = true;
488
+ } else {
489
+ if (newFd >= 0) fs4.closeSync(newFd);
490
+ try {
491
+ const r = fs4.openSync(`/dev/fd/${savedFd2}`, "a");
492
+ if (r !== 2 && r >= 0) fs4.closeSync(r);
493
+ } catch {
494
+ }
495
+ try {
496
+ fs4.closeSync(savedFd2);
497
+ } catch {
498
+ }
499
+ savedFd2 = -1;
500
+ }
501
+ }
502
+ } catch {
503
+ }
504
+ }
505
+ console.log = createConsolePatch("log", logFile, hasSufficientDiskSpace);
506
+ console.info = createConsolePatch("info", logFile, hasSufficientDiskSpace);
507
+ console.warn = createConsolePatch("warn", logFile, hasSufficientDiskSpace);
508
+ console.error = createConsolePatch("error", logFile, hasSufficientDiskSpace);
509
+ console.debug = createConsolePatch("debug", logFile, hasSufficientDiskSpace);
510
+ process.stderr.write = (chunk, encodingOrCb, callback) => {
511
+ const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
512
+ const message = typeof chunk === "string" ? chunk : decoder.decode(chunk);
513
+ if (!hasSufficientDiskSpace()) {
514
+ if (typeof cb === "function") cb();
515
+ return true;
516
+ }
517
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
518
+ const entry = {
519
+ timestamp,
520
+ level: "STDERR",
521
+ ...getLogContext(),
522
+ message: redactSecrets(message.trim())
523
+ };
524
+ try {
525
+ fs4.appendFileSync(logFile, `${safeJsonStringify(entry)}
526
+ `);
527
+ } catch {
528
+ }
529
+ if (typeof cb === "function") cb();
530
+ return true;
531
+ };
532
+ process.stdout.write = (chunk, encodingOrCb, callback) => {
533
+ const message = typeof chunk === "string" ? chunk : decoder.decode(chunk);
534
+ const isNative = isNativeLog(message);
535
+ const complexTui = isComplexTui(message);
536
+ const boxDrawing = isBoxDrawing(message);
537
+ const hasAnsi = message.includes("\x1B[");
538
+ if (isNative && !boxDrawing) {
539
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
540
+ const entry = {
541
+ timestamp,
542
+ level: "STDOUT_NATIVE",
543
+ ...getLogContext(),
544
+ message: redactSecrets(message.trim())
545
+ };
546
+ try {
547
+ fs4.appendFileSync(logFile, `${safeJsonStringify(entry)}
548
+ `);
549
+ } catch {
550
+ }
551
+ const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
552
+ if (typeof cb === "function") cb();
553
+ return true;
554
+ }
555
+ if (complexTui || boxDrawing) {
556
+ return originalStdoutWrite.call(process.stdout, chunk, encodingOrCb, callback);
557
+ }
558
+ if (!hasAnsi && message.trim().length > 0) {
559
+ if (!hasSufficientDiskSpace()) {
560
+ const cb2 = typeof encodingOrCb === "function" ? encodingOrCb : callback;
561
+ if (typeof cb2 === "function") cb2();
562
+ return true;
563
+ }
564
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
565
+ const entry = {
566
+ timestamp,
567
+ level: "STDOUT_PLAIN",
568
+ ...getLogContext(),
569
+ message: redactSecrets(message.trim())
570
+ };
571
+ try {
572
+ fs4.appendFileSync(logFile, `${safeJsonStringify(entry)}
573
+ `);
574
+ } catch {
575
+ }
576
+ const cb = typeof encodingOrCb === "function" ? encodingOrCb : callback;
577
+ if (typeof cb === "function") cb();
578
+ return true;
579
+ }
580
+ return originalStdoutWrite.call(process.stdout, chunk, encodingOrCb, callback);
581
+ };
582
+ try {
583
+ const descriptor = Object.getOwnPropertyDescriptor(fs4, "writeSync");
584
+ if (!descriptor || (descriptor.writable || descriptor.set)) {
585
+ fs4.writeSync = (fd, chunk, ...args) => {
586
+ if (fd === 1 || fd === 2) {
587
+ const message = typeof chunk === "string" ? chunk : decoder.decode(chunk);
588
+ const isNative = isNativeLog(message);
589
+ const boxDrawing = isBoxDrawing(message);
590
+ const complexTui = isComplexTui(message);
591
+ const hasAnsi = message.includes("\x1B[");
592
+ const shouldDivert = fd === 2 || isNative && !boxDrawing && !complexTui || !hasAnsi && !boxDrawing && message.trim().length > 0;
593
+ if (shouldDivert) {
594
+ if (!hasSufficientDiskSpace()) {
595
+ return typeof chunk === "string" ? Buffer.from(chunk).length : chunk.length;
596
+ }
597
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
598
+ const entry = {
599
+ timestamp,
600
+ level: fd === 1 ? "FS_WRITE_SYNC_STDOUT" : "FS_WRITE_SYNC_STDERR",
601
+ ...getLogContext(),
602
+ message: redactSecrets(message.trim())
603
+ };
604
+ try {
605
+ fs4.appendFileSync(logFile, `${safeJsonStringify(entry)}
606
+ `);
607
+ } catch {
608
+ }
609
+ return typeof chunk === "string" ? Buffer.from(chunk).length : chunk.length;
610
+ }
611
+ }
612
+ return originalFsWriteSync.apply(fs4, [fd, chunk, ...args]);
613
+ };
614
+ }
615
+ } catch (_e) {
616
+ }
617
+ try {
618
+ return await task();
619
+ } finally {
620
+ process.stderr.write = originalStderrWrite;
621
+ process.stdout.write = originalStdoutWrite;
622
+ try {
623
+ fs4.writeSync = originalFsWriteSync;
624
+ } catch {
625
+ }
626
+ console.log = originalConsole.log;
627
+ console.info = originalConsole.info;
628
+ console.warn = originalConsole.warn;
629
+ console.error = originalConsole.error;
630
+ console.debug = originalConsole.debug;
631
+ if (sessionId) {
632
+ setSessionCapturing(sessionId, false);
633
+ } else {
634
+ unkeyedCaptureDepth = Math.max(0, unkeyedCaptureDepth - 1);
635
+ recomputeGlobalCapturing();
636
+ }
637
+ if (fd2Redirected && savedFd2 >= 0) {
638
+ try {
639
+ fs4.closeSync(2);
640
+ const r = fs4.openSync(`/dev/fd/${savedFd2}`, "a");
641
+ if (r !== 2 && r >= 0) {
642
+ }
643
+ try {
644
+ fs4.closeSync(savedFd2);
645
+ } catch {
646
+ }
647
+ } catch {
648
+ }
649
+ }
650
+ }
651
+ }
652
+ var fs4, sessionCaptureStates, unkeyedCaptureDepth, isAnyLoggerCapturingOutput, NATIVE_LOG_PATTERNS, COMPLEX_TUI_PATTERNS, BOX_DRAWING_CHARS;
653
+ var init_stdio_capture = __esm({
654
+ "src/utils/stdio-capture.ts"() {
655
+ "use strict";
656
+ init_log_utils();
657
+ fs4 = createRequire(import.meta.url)("node:fs");
658
+ sessionCaptureStates = /* @__PURE__ */ new Map();
659
+ unkeyedCaptureDepth = 0;
660
+ isAnyLoggerCapturingOutput = false;
661
+ NATIVE_LOG_PATTERNS = [
662
+ "Warning:",
663
+ "Error:",
664
+ "Dawn",
665
+ "ONNX",
666
+ "ort",
667
+ "maxDynamicStorageBuffersPerPipelineLayout",
668
+ "maxComputeWorkgroupStorageSize",
669
+ "allocation limit",
670
+ "artificially",
671
+ "reduced from",
672
+ "dynamic offset allocation limit"
673
+ ];
674
+ COMPLEX_TUI_PATTERNS = [
675
+ "\x1B[H",
676
+ // Home
677
+ "\x1B[2J",
678
+ // Clear screen
679
+ "\x1B[J",
680
+ // Clear to end
681
+ "\x1B[K",
682
+ // Clear line
683
+ "\x1B[?25",
684
+ // Cursor visibility
685
+ "\x1B[?1000",
686
+ // Mouse tracking
687
+ "\x1B[?1002",
688
+ "\x1B[?1003",
689
+ "\x1B[?1006",
690
+ "\x1B[?2004",
691
+ // Bracketed paste
692
+ "\x1B[<u",
693
+ // Kitty protocol
694
+ "\x1B[>4;0m"
695
+ // xterm modifyOtherKeys
696
+ ];
697
+ BOX_DRAWING_CHARS = /[─│┌┐└┘├┤┬┴┼═║╔╗╚╝╠╣╦╩╬╴╵╶╷╭╮╯╰╱╲╳]/;
698
+ }
699
+ });
700
+
701
+ // src/logger.ts
702
+ import { appendFileSync, existsSync as existsSync2, mkdirSync, openSync, closeSync } from "node:fs";
703
+ import { appendFile as appendFile2 } from "node:fs/promises";
704
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "node:async_hooks";
705
+ import * as path3 from "node:path";
706
+ function getLogWriter(filePath) {
707
+ let writer = logWriters.get(filePath);
708
+ if (!writer) {
709
+ writer = new LogWriter(filePath);
710
+ logWriters.set(filePath, writer);
711
+ }
712
+ if (!exitDrainRegistered) {
713
+ exitDrainRegistered = true;
714
+ process.on("exit", () => {
715
+ for (const w of logWriters.values()) {
716
+ try {
717
+ w.drainSync();
718
+ } catch {
719
+ }
720
+ }
721
+ });
722
+ }
723
+ return writer;
724
+ }
725
+ function getLogger(sessionId) {
726
+ const contextLogger = loggerContext.getStore();
727
+ if (contextLogger) return contextLogger;
728
+ if (sessionId) {
729
+ let lg = sessionLoggers.get(sessionId);
730
+ if (!lg) {
731
+ lg = new Logger({
732
+ verbose: isVerboseFromEnv(),
733
+ researchRunId: sessionId,
734
+ consoleLog: process.env["PI_RESEARCH_CONSOLE_LOG"] === "true"
735
+ });
736
+ sessionLoggers.set(sessionId, lg);
737
+ }
738
+ return lg;
739
+ }
740
+ if (!_globalLogger) {
741
+ _globalLogger = new Logger({
742
+ verbose: isVerboseFromEnv(),
743
+ consoleLog: process.env["PI_RESEARCH_CONSOLE_LOG"] === "true"
744
+ });
745
+ }
746
+ return _globalLogger;
747
+ }
748
+ var LogWriter, logWriters, exitDrainRegistered, LOGGER_BRAND, Logger, loggerContext, sessionLoggers, _globalLogger, logger;
749
+ var init_logger = __esm({
750
+ "src/logger.ts"() {
751
+ "use strict";
752
+ init_error_tracker();
753
+ init_log_utils();
754
+ init_log_rotation();
755
+ init_disk_space_checker();
756
+ init_stdio_capture();
757
+ init_log_utils();
758
+ LogWriter = class _LogWriter {
759
+ constructor(filePath) {
760
+ this.filePath = filePath;
761
+ }
762
+ filePath;
763
+ buffer = [];
764
+ bufferBytes = 0;
765
+ flushScheduled = false;
766
+ flushing = false;
767
+ // Flush eagerly once buffered output gets large, to bound memory and latency
768
+ // during sustained bursts instead of waiting for the scheduled tick.
769
+ static MAX_BUFFER_BYTES = 256 * 1024;
770
+ /** Queue a line for asynchronous, non-blocking write (INFO/DEBUG path). */
771
+ enqueue(line) {
772
+ this.buffer.push(line);
773
+ this.bufferBytes += line.length;
774
+ if (this.bufferBytes >= _LogWriter.MAX_BUFFER_BYTES) {
775
+ void this.flush();
776
+ return;
777
+ }
778
+ this.scheduleFlush();
779
+ }
780
+ scheduleFlush() {
781
+ if (this.flushScheduled || this.flushing) return;
782
+ this.flushScheduled = true;
783
+ setTimeout(() => {
784
+ void this.flush();
785
+ }, 0);
786
+ }
787
+ /** Asynchronously write buffered lines. Never blocks the event loop. */
788
+ async flush() {
789
+ this.flushScheduled = false;
790
+ if (this.flushing || this.buffer.length === 0) return;
791
+ this.flushing = true;
792
+ try {
793
+ while (this.buffer.length > 0) {
794
+ const batch = this.buffer.join("");
795
+ this.buffer.length = 0;
796
+ this.bufferBytes = 0;
797
+ try {
798
+ await appendFile2(this.filePath, batch);
799
+ } catch {
800
+ }
801
+ }
802
+ } finally {
803
+ this.flushing = false;
804
+ if (this.buffer.length > 0) this.scheduleFlush();
805
+ }
806
+ }
807
+ /**
808
+ * Drain buffered lines, then write `line`, all synchronously and in order.
809
+ * Used for WARN/ERROR so the critical line (and all lines queued in the buffer
810
+ * before it) are on disk before this call returns.
811
+ *
812
+ * Ordering caveat: if a large INFO/DEBUG burst already cleared the buffer into
813
+ * an in-flight async appendFile, drainSync() will find the buffer empty and
814
+ * the WARN line will be appended before that in-flight batch completes. In
815
+ * practice this means WARN/ERROR lines may appear slightly before the last
816
+ * flush of the preceding INFO/DEBUG burst. Acceptable for a diagnostic log.
817
+ */
818
+ writeSync(line) {
819
+ this.drainSync();
820
+ try {
821
+ appendFileSync(this.filePath, line);
822
+ } catch {
823
+ }
824
+ }
825
+ /** Synchronously flush all buffered lines (graceful exit / tests / pre-sync-write). */
826
+ drainSync() {
827
+ if (this.buffer.length === 0) return;
828
+ const batch = this.buffer.join("");
829
+ this.buffer.length = 0;
830
+ this.bufferBytes = 0;
831
+ try {
832
+ appendFileSync(this.filePath, batch);
833
+ } catch {
834
+ }
835
+ }
836
+ };
837
+ logWriters = /* @__PURE__ */ new Map();
838
+ exitDrainRegistered = false;
839
+ LOGGER_BRAND = /* @__PURE__ */ Symbol.for("pi-research.Logger");
840
+ Logger = class {
841
+ verbose;
842
+ consoleLog;
843
+ logFile;
844
+ logDir;
845
+ sessionId;
846
+ [LOGGER_BRAND] = true;
847
+ rotation;
848
+ diskSpaceChecker;
849
+ constructor(options = {}) {
850
+ this.verbose = options.verbose ?? isVerboseFromEnv();
851
+ this.consoleLog = options.consoleLog ?? process.env["PI_RESEARCH_CONSOLE_LOG"] === "true";
852
+ this.logFile = options.logFilePath ?? buildDefaultDebugLogPath();
853
+ this.logDir = path3.dirname(this.logFile);
854
+ this.sessionId = options.researchRunId;
855
+ this.rotation = new LogRotation(this);
856
+ this.diskSpaceChecker = new DiskSpaceChecker();
857
+ try {
858
+ if (!existsSync2(this.logDir)) {
859
+ mkdirSync(this.logDir, { recursive: true, mode: 448 });
860
+ }
861
+ try {
862
+ closeSync(openSync(this.logFile, "wx", 384));
863
+ } catch {
864
+ }
865
+ } catch {
866
+ }
867
+ }
868
+ emit(level, ...args) {
869
+ if (!this.verbose && (level === "INFO" /* INFO */ || level === "DEBUG" /* DEBUG */)) {
870
+ return;
871
+ }
872
+ if (!this.diskSpaceChecker.checkDiskSpace(this.logDir)) {
873
+ return;
874
+ }
875
+ this.rotation.checkAndRotate(this.logFile, this.logDir);
876
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
877
+ const firstError = args.find((arg) => arg instanceof Error);
878
+ const message = redactSecrets(args.map(formatArg).join(" "));
879
+ const entry = {
880
+ timestamp,
881
+ level,
882
+ ...getLogContext(),
883
+ message,
884
+ ...firstError ? { errorMessage: redactSecrets(firstError.message), errorStack: redactSecrets(firstError.stack ?? "") } : {}
885
+ };
886
+ const line = `${safeJsonStringify(entry)}
887
+ `;
888
+ const writer = getLogWriter(this.logFile);
889
+ if (level === "ERROR" /* ERROR */ || level === "WARN" /* WARN */) {
890
+ writer.writeSync(line);
891
+ } else {
892
+ writer.enqueue(line);
893
+ }
894
+ if (this.consoleLog) {
895
+ const color = level === "ERROR" /* ERROR */ ? "\x1B[31m" : level === "WARN" /* WARN */ ? "\x1B[33m" : level === "DEBUG" /* DEBUG */ ? "\x1B[90m" : "\x1B[36m";
896
+ const reset = "\x1B[0m";
897
+ const msg = message.replace(/[\r\n]/g, "");
898
+ const rawSid = this.sessionId ?? "";
899
+ const sid = rawSid.replace(/[\r\n]/g, "");
900
+ const prefix = sid ? `[${sid}] ` : "";
901
+ console.log(`${color}${timestamp} ${level} ${prefix}${reset}${msg}`);
902
+ }
903
+ }
904
+ async runCapturingStderr(task) {
905
+ return captureStdio(
906
+ this.logFile,
907
+ () => this.diskSpaceChecker.checkDiskSpace(this.logDir),
908
+ task,
909
+ this.sessionId
910
+ );
911
+ }
912
+ log(...args) {
913
+ this.emit("INFO" /* INFO */, ...args);
914
+ }
915
+ info(...args) {
916
+ this.emit("INFO" /* INFO */, ...args);
917
+ }
918
+ error(...args) {
919
+ const errArg = args.find((arg) => arg instanceof Error);
920
+ if (errArg) {
921
+ const context2 = getLogContext();
922
+ errorTracker.trackError(errArg, context2);
923
+ }
924
+ this.emit("ERROR" /* ERROR */, ...args);
925
+ }
926
+ warn(...args) {
927
+ this.emit("WARN" /* WARN */, ...args);
928
+ }
929
+ debug(...args) {
930
+ this.emit("DEBUG" /* DEBUG */, ...args);
931
+ }
932
+ clear() {
933
+ this.flushSync();
934
+ this.rotation.clearLogs(this.logFile, this.logDir);
935
+ }
936
+ /**
937
+ * Synchronously flush this logger's buffered INFO/DEBUG output to disk.
938
+ * Use before reading the log file directly (tests) or during shutdown.
939
+ */
940
+ flushSync() {
941
+ getLogWriter(this.logFile).drainSync();
942
+ }
943
+ getLogFilePath() {
944
+ return this.logFile;
945
+ }
946
+ isVerbose() {
947
+ return this.verbose;
948
+ }
949
+ };
950
+ loggerContext = new AsyncLocalStorage3();
951
+ sessionLoggers = /* @__PURE__ */ new Map();
952
+ _globalLogger = null;
953
+ logger = {
954
+ log: (...args) => getLogger().log(...args),
955
+ info: (...args) => getLogger().info(...args),
956
+ error: (...args) => getLogger().error(...args),
957
+ warn: (...args) => getLogger().warn(...args),
958
+ debug: (...args) => getLogger().debug(...args),
959
+ clear: () => getLogger().clear(),
960
+ flushSync: () => getLogger().flushSync(),
961
+ runCapturingStderr: (task) => getLogger().runCapturingStderr(task)
962
+ };
963
+ }
964
+ });
965
+
966
+ // src/utils/metrics.ts
967
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "node:async_hooks";
968
+ var MetricsRegistry, runRegistryStorage, MAX_RUN_HISTORY, SessionMetrics, metrics;
969
+ var init_metrics = __esm({
970
+ "src/utils/metrics.ts"() {
971
+ "use strict";
972
+ init_logger();
973
+ MetricsRegistry = class {
974
+ counters = /* @__PURE__ */ new Map();
975
+ gauges = /* @__PURE__ */ new Map();
976
+ histograms = /* @__PURE__ */ new Map();
977
+ serializeLabels(labels) {
978
+ if (!labels) return "";
979
+ const keys = Object.keys(labels).sort();
980
+ return keys.map((k) => `${k}="${labels[k]}"`).join(",");
981
+ }
982
+ getKey(name, labels) {
983
+ const labelStr = this.serializeLabels(labels);
984
+ return labelStr ? `${name}{${labelStr}}` : name;
985
+ }
986
+ increment(name, value = 1, labels) {
987
+ const key = this.getKey(name, labels);
988
+ this.counters.set(key, (this.counters.get(key) ?? 0) + value);
989
+ if (name === "llm_cost_total" || name === "llm_tokens_total") {
990
+ logger.debug(`[Metrics] Incremented ${name} by ${value}${labels ? ` (labels: ${JSON.stringify(labels)})` : ""}`);
991
+ }
992
+ }
993
+ setGauge(name, value, labels) {
994
+ this.gauges.set(this.getKey(name, labels), value);
995
+ }
996
+ observe(name, value, labels) {
997
+ if (!Number.isFinite(value)) return;
998
+ const key = this.getKey(name, labels);
999
+ let entry = this.histograms.get(key);
1000
+ if (!entry) {
1001
+ entry = { values: [], pointer: 0, limit: 1e4, dirty: true, cached: null };
1002
+ this.histograms.set(key, entry);
1003
+ }
1004
+ entry.dirty = true;
1005
+ if (entry.values.length < entry.limit) {
1006
+ entry.values.push(value);
1007
+ } else {
1008
+ entry.values[entry.pointer] = value;
1009
+ entry.pointer = (entry.pointer + 1) % entry.limit;
1010
+ }
1011
+ }
1012
+ async measure(name, action, labels) {
1013
+ const start = process.hrtime.bigint();
1014
+ try {
1015
+ const result = await action();
1016
+ this.observe(name, Number(process.hrtime.bigint() - start) / 1e6, labels);
1017
+ return result;
1018
+ } catch (error) {
1019
+ this.observe(name, Number(process.hrtime.bigint() - start) / 1e6, { ...labels, error: "true" });
1020
+ this.increment(`${name}_errors_total`, 1, labels);
1021
+ throw error;
1022
+ }
1023
+ }
1024
+ percentile(sorted, pct) {
1025
+ const len = sorted.length;
1026
+ if (len === 0) return 0;
1027
+ if (len === 1) return sorted[0] ?? 0;
1028
+ if (pct <= 0) return sorted[0] ?? 0;
1029
+ if (pct >= 100) return sorted[len - 1] ?? 0;
1030
+ const rank = pct / 100 * (len - 1);
1031
+ const lo = Math.floor(rank);
1032
+ const hi = Math.ceil(rank);
1033
+ const w = rank - lo;
1034
+ return (sorted[lo] ?? 0) + ((sorted[hi] ?? 0) - (sorted[lo] ?? 0)) * w;
1035
+ }
1036
+ getSnapshot() {
1037
+ const snap = { counters: {}, gauges: {}, histograms: {} };
1038
+ for (const [k, v] of this.counters) snap.counters[k] = v;
1039
+ for (const [k, v] of this.gauges) snap.gauges[k] = v;
1040
+ for (const [k, entry] of this.histograms) {
1041
+ if (entry.values.length === 0) continue;
1042
+ if (!entry.dirty && entry.cached) {
1043
+ snap.histograms[k] = entry.cached;
1044
+ continue;
1045
+ }
1046
+ const vals = entry.values;
1047
+ const sorted = vals.slice().sort((a, b) => a - b);
1048
+ const sum = sorted.reduce((a, b) => a + b, 0);
1049
+ const histogram = {
1050
+ count: sorted.length,
1051
+ min: sorted[0] ?? 0,
1052
+ max: sorted[sorted.length - 1] ?? 0,
1053
+ avg: sum / sorted.length,
1054
+ p50: this.percentile(sorted, 50),
1055
+ p90: this.percentile(sorted, 90),
1056
+ p95: this.percentile(sorted, 95),
1057
+ p99: this.percentile(sorted, 99)
1058
+ };
1059
+ entry.cached = histogram;
1060
+ entry.dirty = false;
1061
+ snap.histograms[k] = histogram;
1062
+ }
1063
+ return snap;
1064
+ }
1065
+ clear() {
1066
+ this.counters.clear();
1067
+ this.gauges.clear();
1068
+ this.histograms.clear();
1069
+ }
1070
+ };
1071
+ runRegistryStorage = new AsyncLocalStorage4();
1072
+ MAX_RUN_HISTORY = 10;
1073
+ SessionMetrics = class {
1074
+ _session = new MetricsRegistry();
1075
+ _sessionStartedAt = Date.now();
1076
+ _runHistory = [];
1077
+ getActive() {
1078
+ return runRegistryStorage.getStore() ?? this._session;
1079
+ }
1080
+ // ── Emit API (routes to active registry) ──────────────────────────────
1081
+ increment(name, value = 1, labels) {
1082
+ this.getActive().increment(name, value, labels);
1083
+ }
1084
+ setGauge(name, value, labels) {
1085
+ this.getActive().setGauge(name, value, labels);
1086
+ }
1087
+ observe(name, value, labels) {
1088
+ this.getActive().observe(name, value, labels);
1089
+ }
1090
+ async measure(name, action, labels) {
1091
+ return this.getActive().measure(name, action, labels);
1092
+ }
1093
+ // ── Session-level read API ─────────────────────────────────────────────
1094
+ /** Snapshot of the session registry (infrastructure / cross-run metrics). */
1095
+ getSessionSnapshot() {
1096
+ return this._session.getSnapshot();
1097
+ }
1098
+ /** Millisecond timestamp when this session started or was last reset. */
1099
+ getSessionStartedAt() {
1100
+ return this._sessionStartedAt;
1101
+ }
1102
+ /**
1103
+ * Ordered run summaries, oldest first. Capped at MAX_RUN_HISTORY entries.
1104
+ * Returns a shallow copy so callers cannot mutate the internal list.
1105
+ */
1106
+ getRunHistory() {
1107
+ return [...this._runHistory];
1108
+ }
1109
+ // ── Run lifecycle API ──────────────────────────────────────────────────
1110
+ /**
1111
+ * Append a completed run's snapshot to the history.
1112
+ * Call this AFTER runWithRunRegistry returns so the write lands in session
1113
+ * scope. Automatically evicts the oldest entry when the cap is reached.
1114
+ */
1115
+ recordRunSummary(summary) {
1116
+ this._runHistory.push(summary);
1117
+ if (this._runHistory.length > MAX_RUN_HISTORY) {
1118
+ this._runHistory.shift();
1119
+ }
1120
+ }
1121
+ // ── Reset API ──────────────────────────────────────────────────────────
1122
+ /**
1123
+ * Clear the session registry, run history, and session start timestamp.
1124
+ * In-flight run registries are NOT affected.
1125
+ */
1126
+ clearSession() {
1127
+ this._session.clear();
1128
+ this._runHistory = [];
1129
+ this._sessionStartedAt = Date.now();
1130
+ }
1131
+ };
1132
+ metrics = new SessionMetrics();
1133
+ }
1134
+ });
1135
+
1136
+ // src/utils/user-agent.ts
1137
+ var USER_AGENTS;
1138
+ var init_user_agent = __esm({
1139
+ "src/utils/user-agent.ts"() {
1140
+ "use strict";
1141
+ USER_AGENTS = [
1142
+ // Chrome on Windows (updated 2025)
1143
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
1144
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
1145
+ // Chrome on macOS
1146
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
1147
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
1148
+ // Chrome on Linux
1149
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36",
1150
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36",
1151
+ // Firefox on Windows (updated 2025)
1152
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:139.0) Gecko/20100101 Firefox/139.0",
1153
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:138.0) Gecko/20100101 Firefox/138.0",
1154
+ // Firefox on macOS
1155
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:139.0) Gecko/20100101 Firefox/139.0",
1156
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:138.0) Gecko/20100101 Firefox/138.0"
1157
+ ];
1158
+ }
1159
+ });
1160
+
1161
+ // src/web-research/scraper-types.ts
1162
+ var MAX_HTML_SIZE, MAX_PDF_SIZE, INTERNAL_NETWORK_PATTERNS, FILTERED_TAGS, BOT_PATTERNS, IMAGE_LINK_PATTERN, MARKDOWN_IMAGE_PATTERN;
1163
+ var init_scraper_types = __esm({
1164
+ "src/web-research/scraper-types.ts"() {
1165
+ "use strict";
1166
+ init_user_agent();
1167
+ MAX_HTML_SIZE = 25 * 1024 * 1024;
1168
+ MAX_PDF_SIZE = 100 * 1024 * 1024;
1169
+ INTERNAL_NETWORK_PATTERNS = [
1170
+ /^127\./,
1171
+ // IPv4 loopback
1172
+ /^0\./,
1173
+ // IPv4 "this" network
1174
+ /^::1$/,
1175
+ // IPv6 loopback
1176
+ /^fe80::/i,
1177
+ // IPv6 link-local
1178
+ /^f[cd][0-9a-f]{2}:/i,
1179
+ // IPv6 unique local — full fc00::/7 block (fc00::–fdff::)
1180
+ /^169\.254\./,
1181
+ // IPv4 link-local (and metadata)
1182
+ /^10\./,
1183
+ // RFC 1918 Class A private
1184
+ /^192\.168\./,
1185
+ // RFC 1918 Class C private
1186
+ /^172\.(1[6-9]|2[0-9]|3[01])\./,
1187
+ // RFC 1918 Class B private
1188
+ /^::ffff:(127\.|0\.|169\.254\.|10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[01])\.)/i
1189
+ // IPv4-mapped IPv6
1190
+ ];
1191
+ FILTERED_TAGS = [
1192
+ "nav",
1193
+ "header",
1194
+ "footer",
1195
+ "aside",
1196
+ "script",
1197
+ "style",
1198
+ "noscript",
1199
+ "form",
1200
+ "input",
1201
+ "select",
1202
+ "textarea",
1203
+ "button",
1204
+ "object",
1205
+ "embed",
1206
+ "svg",
1207
+ "symbol",
1208
+ "use",
1209
+ "defs",
1210
+ "path",
1211
+ "circle",
1212
+ "rect",
1213
+ "line",
1214
+ "polygon",
1215
+ "img",
1216
+ "iframe"
1217
+ ];
1218
+ BOT_PATTERNS = [
1219
+ ["_cf_chl_", "Cloudflare challenge"],
1220
+ ["cf_chl_opt", "Cloudflare challenge opt"],
1221
+ ["cdn-cgi/challenge-platform", "Cloudflare challenge platform"],
1222
+ ["ddos-guard", "DDoS-Guard challenge"],
1223
+ ["Just a moment...", "Cloudflare interstitial"],
1224
+ ["Checking your browser before accessing", "Cloudflare interstitial"]
1225
+ ];
1226
+ IMAGE_LINK_PATTERN = /\[([^\]]*)\]\((data:image\/[^)]+|[^)\s]+\.(?:svg|png|jpe?g|gif|webp|bmp|ico)(?:\?[^)]*)?)\)/gi;
1227
+ MARKDOWN_IMAGE_PATTERN = /!\[[^\]]*]\((?:data:image\/[^)]+|[^)\s]+)\)/gi;
1228
+ }
1229
+ });
1230
+
1231
+ // src/utils/circuit-breaker.ts
1232
+ var CircuitBreaker;
1233
+ var init_circuit_breaker = __esm({
1234
+ "src/utils/circuit-breaker.ts"() {
1235
+ "use strict";
1236
+ init_logger();
1237
+ init_metrics();
1238
+ CircuitBreaker = class {
1239
+ state = "CLOSED";
1240
+ failureCount = 0;
1241
+ successCount = 0;
1242
+ nextAttemptTime = 0;
1243
+ // Number of trial calls currently executing in HALF_OPEN. Caps concurrent probes so a burst of
1244
+ // parallel callers doesn't all hit a still-recovering dependency the instant the breaker half-opens.
1245
+ halfOpenInFlight = 0;
1246
+ options;
1247
+ constructor(options = {}) {
1248
+ this.options = {
1249
+ failureThreshold: options.failureThreshold ?? 5,
1250
+ resetTimeoutMs: options.resetTimeoutMs ?? 3e4,
1251
+ halfOpenMaxCalls: options.halfOpenMaxCalls ?? 1,
1252
+ name: options.name ?? "CircuitBreaker",
1253
+ isTransientError: options.isTransientError ?? (() => true)
1254
+ };
1255
+ }
1256
+ async execute(action) {
1257
+ const startTime = Date.now();
1258
+ metrics.increment("circuit_breaker_calls_total", 1, { breaker: this.options.name, state: this.state });
1259
+ if (this.state === "OPEN") {
1260
+ if (Date.now() >= this.nextAttemptTime) {
1261
+ this.transitionTo("HALF_OPEN");
1262
+ } else {
1263
+ metrics.increment("circuit_breaker_rejected_total", 1, { breaker: this.options.name, reason: "open" });
1264
+ throw new Error(`CircuitBreaker '${this.options.name}' is OPEN. Fast-failing to prevent cascading failure.`);
1265
+ }
1266
+ }
1267
+ let admittedAsProbe = false;
1268
+ if (this.state === "HALF_OPEN") {
1269
+ if (this.halfOpenInFlight >= this.options.halfOpenMaxCalls) {
1270
+ metrics.increment("circuit_breaker_rejected_total", 1, { breaker: this.options.name, reason: "half_open_probe_limit" });
1271
+ throw new Error(`CircuitBreaker '${this.options.name}' is HALF_OPEN (trial in progress). Fast-failing to limit probes.`);
1272
+ }
1273
+ this.halfOpenInFlight++;
1274
+ admittedAsProbe = true;
1275
+ }
1276
+ try {
1277
+ const result = await action();
1278
+ const duration = Date.now() - startTime;
1279
+ metrics.observe("circuit_breaker_call_duration_ms", duration, { breaker: this.options.name, status: "success" });
1280
+ this.onSuccess();
1281
+ return result;
1282
+ } catch (error) {
1283
+ const duration = Date.now() - startTime;
1284
+ metrics.observe("circuit_breaker_call_duration_ms", duration, { breaker: this.options.name, status: "error" });
1285
+ if (this.options.isTransientError(error)) {
1286
+ this.onFailure(error);
1287
+ }
1288
+ throw error;
1289
+ } finally {
1290
+ if (admittedAsProbe) this.halfOpenInFlight = Math.max(0, this.halfOpenInFlight - 1);
1291
+ }
1292
+ }
1293
+ onSuccess() {
1294
+ metrics.increment("circuit_breaker_success_total", 1, { breaker: this.options.name, state: this.state });
1295
+ if (this.state === "HALF_OPEN") {
1296
+ this.successCount++;
1297
+ if (this.successCount >= this.options.halfOpenMaxCalls) {
1298
+ this.transitionTo("CLOSED");
1299
+ }
1300
+ } else if (this.state === "CLOSED") {
1301
+ this.failureCount = 0;
1302
+ }
1303
+ }
1304
+ onFailure(error) {
1305
+ this.failureCount++;
1306
+ const errMsg = error instanceof Error ? error.message : String(error);
1307
+ metrics.increment("circuit_breaker_failures_total", 1, { breaker: this.options.name, state: this.state });
1308
+ if (this.state === "HALF_OPEN") {
1309
+ logger.warn(`[CircuitBreaker] '${this.options.name}' failed during HALF_OPEN state: ${errMsg}`);
1310
+ this.transitionTo("OPEN");
1311
+ } else if (this.state === "CLOSED") {
1312
+ if (this.failureCount >= this.options.failureThreshold) {
1313
+ logger.error(`[CircuitBreaker] '${this.options.name}' reached failure threshold (${this.failureCount}): ${errMsg}`);
1314
+ this.transitionTo("OPEN");
1315
+ }
1316
+ }
1317
+ }
1318
+ transitionTo(newState) {
1319
+ logger.warn(`[CircuitBreaker] '${this.options.name}' transitioning from ${this.state} to ${newState}`);
1320
+ metrics.increment("circuit_breaker_state_transitions_total", 1, { breaker: this.options.name, from: this.state, to: newState });
1321
+ metrics.setGauge("circuit_breaker_state", newState === "CLOSED" ? 0 : newState === "OPEN" ? 1 : 2, { breaker: this.options.name });
1322
+ this.state = newState;
1323
+ if (newState === "OPEN") {
1324
+ this.nextAttemptTime = Date.now() + this.options.resetTimeoutMs;
1325
+ } else if (newState === "HALF_OPEN") {
1326
+ this.successCount = 0;
1327
+ this.halfOpenInFlight = 0;
1328
+ } else if (newState === "CLOSED") {
1329
+ this.failureCount = 0;
1330
+ this.successCount = 0;
1331
+ }
1332
+ }
1333
+ getState() {
1334
+ return this.state;
1335
+ }
1336
+ reset() {
1337
+ metrics.increment("circuit_breaker_resets_total", 1, { breaker: this.options.name });
1338
+ this.state = "CLOSED";
1339
+ this.failureCount = 0;
1340
+ this.successCount = 0;
1341
+ this.halfOpenInFlight = 0;
1342
+ this.nextAttemptTime = 0;
1343
+ metrics.setGauge("circuit_breaker_state", 0, { breaker: this.options.name });
1344
+ }
1345
+ };
1346
+ }
1347
+ });
1348
+
1349
+ // src/infrastructure/browser/browser-error-utils.ts
1350
+ function isTransientSocketError(error) {
1351
+ if (!error || typeof error !== "object") return false;
1352
+ const err = error;
1353
+ return typeof err.message === "string" && (err.message.includes("ECONNREFUSED") || err.message.includes("ECONNRESET") || err.message.includes("socket hang up") || err.message.includes("EPIPE") || err.message.includes("ETIMEDOUT") || err.message.includes("timed out") || err.message.includes("pool busy") || err.message.includes("unreachable") || // WorkerPoolManager throws this during the window between forceSchedulerRestart
1354
+ // clearing the scheduler reference and the old pool's 1500ms drain completing.
1355
+ // It is a transient state — retry after a short delay succeeds once drain finishes.
1356
+ err.message.includes("Worker pool is shutting down") || // Poolifier throws this when pool.execute() is called while the pool is being
1357
+ // destroyed (e.g., during scheduler restart). Treat as transient — the pool will
1358
+ // be ready for new tasks once destruction completes and a new pool is initialized.
1359
+ err.message.includes("Cannot execute a task on destroying pool") || err.message.includes("destroying pool"));
1360
+ }
1361
+ function isCloudflareBlockError(error) {
1362
+ if (!error || typeof error !== "object") return false;
1363
+ const err = error;
1364
+ return typeof err.message === "string" && (err.message.includes("Fetch blocked: Cloudflare") || err.message.includes("Cloudflare challenge"));
1365
+ }
1366
+ function isTaskTimeoutError(error) {
1367
+ if (!error || typeof error !== "object") return false;
1368
+ const err = error;
1369
+ return typeof err.message === "string" && (err.message.includes("timed out after") || err.message.includes("Search task timed out") || err.message.includes("Scrape task timed out") || err.message.includes("Health check timed out") || err.message.includes("[BrowserClient] Request to"));
1370
+ }
1371
+ function isPoolShutdownError(error) {
1372
+ if (!error || typeof error !== "object") return false;
1373
+ const err = error;
1374
+ return typeof err.message === "string" && (err.message.includes("Worker pool is shutting down") || err.message.includes("Cannot execute a task on destroying pool") || err.message.includes("destroying pool"));
1375
+ }
1376
+ var DEFAULT_BREAKER_CONFIG, browserCircuitBreaker;
1377
+ var init_browser_error_utils = __esm({
1378
+ "src/infrastructure/browser/browser-error-utils.ts"() {
1379
+ "use strict";
1380
+ init_circuit_breaker();
1381
+ DEFAULT_BREAKER_CONFIG = {
1382
+ // Raised from 3→8: parallel search bursts of 20 queries frequently produce
1383
+ // simultaneous timeouts on slow/CF-protected sites. A threshold of 3 caused
1384
+ // the breaker to open mid-burst, blocking all subsequent requests for the round.
1385
+ failureThreshold: 8,
1386
+ // Raised from 15s→45s: enough time for one full search burst + retry window
1387
+ // to complete before the breaker attempts to half-open.
1388
+ resetTimeoutMs: 45e3,
1389
+ name: "BrowserPool",
1390
+ isTransientError: (error) => {
1391
+ if (isPoolShutdownError(error)) return false;
1392
+ if (isCloudflareBlockError(error)) return false;
1393
+ if (isTaskTimeoutError(error)) return false;
1394
+ return isTransientSocketError(error);
1395
+ }
1396
+ };
1397
+ browserCircuitBreaker = new CircuitBreaker(DEFAULT_BREAKER_CONFIG);
1398
+ }
1399
+ });
1400
+
1401
+ // src/web-research/scraper-utils.ts
1402
+ var scraper_utils_exports = {};
1403
+ __export(scraper_utils_exports, {
1404
+ createJsMarkdownConverter: () => createJsMarkdownConverter,
1405
+ createNativeMarkdownConverter: () => createNativeMarkdownConverter,
1406
+ extractDomain: () => extractDomain,
1407
+ getRandomUserAgent: () => getRandomUserAgent,
1408
+ getSsrfSafeDispatcher: () => getSsrfSafeDispatcher,
1409
+ isBenignScrapeFailure: () => isBenignScrapeFailure,
1410
+ ssrfSafeLookup: () => ssrfSafeLookup,
1411
+ stripImageLinks: () => stripImageLinks,
1412
+ validateContent: () => validateContent,
1413
+ validateUrlForSSRF: () => validateUrlForSSRF,
1414
+ validateUrlForSSRFSync: () => validateUrlForSSRFSync
1415
+ });
1416
+ import * as dns from "node:dns/promises";
1417
+ import * as dnsCb from "node:dns";
1418
+ import * as net from "node:net";
1419
+ import crypto from "node:crypto";
1420
+ import { NodeHtmlMarkdown } from "node-html-markdown";
1421
+ function isBenignScrapeFailure(error) {
1422
+ const msg = error instanceof Error ? error.message : String(error);
1423
+ return msg.startsWith("Fetch blocked:") || // bot-protection interstitial (Cloudflare, DDoS-Guard, …)
1424
+ msg.startsWith("Fetch returned stub:") || // nav-only / near-empty page
1425
+ /^HTTP \d{3}\b/.test(msg) || // remote 4xx/5xx status
1426
+ isCloudflareBlockError(error) || isTaskTimeoutError(error) || isPoolShutdownError(error);
1427
+ }
1428
+ function getRandomUserAgent() {
1429
+ try {
1430
+ const index = crypto.randomInt(0, USER_AGENTS.length);
1431
+ const userAgent = USER_AGENTS[index];
1432
+ return userAgent ?? USER_AGENTS[0];
1433
+ } catch {
1434
+ return USER_AGENTS[0];
1435
+ }
1436
+ }
1437
+ function extractDomain(url) {
1438
+ try {
1439
+ return new URL(url).hostname;
1440
+ } catch {
1441
+ return "unknown";
1442
+ }
1443
+ }
1444
+ function validateUrlForSSRFSync(url) {
1445
+ let parsed;
1446
+ try {
1447
+ parsed = new URL(url);
1448
+ } catch (e) {
1449
+ metrics.increment("scrape_errors_total", 1, { error_type: "invalid_url" });
1450
+ throw new Error(`Invalid URL: ${url}`, { cause: e });
1451
+ }
1452
+ const hostname = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
1453
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1454
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "invalid_protocol" });
1455
+ throw new Error("Only HTTP/HTTPS protocols are allowed");
1456
+ }
1457
+ if (process.env["PI_RESEARCH_ALLOW_LOOPBACK_SCRAPE"] === "true") {
1458
+ const isLoopback = hostname === "localhost" || hostname.endsWith(".localhost") || /^127\./.test(hostname) || hostname === "::1" || net.isIPv6(hostname) && isPrivateIpv6(hostname) && isMappedLoopback(hostname);
1459
+ if (isLoopback) return true;
1460
+ }
1461
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) {
1462
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "localhost" });
1463
+ throw new Error("Access to localhost is not allowed");
1464
+ }
1465
+ for (const pattern of INTERNAL_NETWORK_PATTERNS) {
1466
+ if (pattern.test(hostname)) {
1467
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "internal_network" });
1468
+ throw new Error("Access to internal networks is not allowed");
1469
+ }
1470
+ }
1471
+ const literalKind = net.isIP(hostname);
1472
+ if (literalKind === 4 && isPrivateIp(hostname)) {
1473
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "ip_literal_v4" });
1474
+ throw new Error("Access to private/reserved IPv4 address is not allowed");
1475
+ }
1476
+ if (literalKind === 6 && isPrivateIpv6(hostname)) {
1477
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "ip_literal_v6" });
1478
+ throw new Error("Access to private/reserved IPv6 address is not allowed");
1479
+ }
1480
+ return false;
1481
+ }
1482
+ async function validateUrlForSSRF(url) {
1483
+ if (validateUrlForSSRFSync(url)) return;
1484
+ const hostname = new URL(url).hostname.toLowerCase().replace(/^\[|\]$/g, "");
1485
+ const [v4Results, v6Results] = await Promise.allSettled([
1486
+ dns.resolve4(hostname),
1487
+ dns.resolve6(hostname)
1488
+ ]);
1489
+ const v4Addresses = v4Results.status === "fulfilled" ? v4Results.value : [];
1490
+ const v6Addresses = v6Results.status === "fulfilled" ? v6Results.value : [];
1491
+ for (const ip of v4Addresses) {
1492
+ if (isPrivateIp(ip)) {
1493
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "dns_rebinding_v4" });
1494
+ throw new Error("Hostname resolves to a private/reserved IPv4 address");
1495
+ }
1496
+ }
1497
+ for (const ip of v6Addresses) {
1498
+ if (isPrivateIpv6(ip)) {
1499
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "dns_rebinding_v6" });
1500
+ throw new Error("Hostname resolves to a private/reserved IPv6 address");
1501
+ }
1502
+ }
1503
+ }
1504
+ function ssrfSafeLookup(hostname, options, callback, resolveImpl = dnsCb.lookup) {
1505
+ const family = typeof options === "number" ? options : options?.family ?? 0;
1506
+ const wantsAll = typeof options === "object" && options?.all === true;
1507
+ const allowLoopback = process.env["PI_RESEARCH_ALLOW_LOOPBACK_SCRAPE"] === "true";
1508
+ const isLoopback = (a) => a.family === 6 ? a.address.toLowerCase() === "::1" || isMappedLoopback(a.address) : /^127\./.test(a.address);
1509
+ resolveImpl(hostname, { all: true, verbatim: true, family }, (err, addresses) => {
1510
+ if (err) return callback(err);
1511
+ const list = Array.isArray(addresses) ? addresses : [];
1512
+ const safe = list.filter((a) => {
1513
+ if (!a) return false;
1514
+ if (allowLoopback && isLoopback(a)) return true;
1515
+ return a.family === 6 ? !isPrivateIpv6(a.address) : !isPrivateIp(a.address);
1516
+ });
1517
+ if (safe.length === 0) {
1518
+ metrics.increment("scrape_ssrf_blocks_total", 1, { block_type: "connect_time_private" });
1519
+ return callback(
1520
+ Object.assign(new Error(`SSRF blocked: ${hostname} resolved only to private/reserved addresses at connect time`), { code: "ESSRFBLOCKED" })
1521
+ );
1522
+ }
1523
+ if (wantsAll) return callback(null, safe);
1524
+ return callback(null, safe[0].address, safe[0].family);
1525
+ });
1526
+ }
1527
+ async function getSsrfSafeDispatcher() {
1528
+ if (cachedSsrfDispatcher !== void 0) return cachedSsrfDispatcher;
1529
+ try {
1530
+ const { Agent } = await import("undici");
1531
+ cachedSsrfDispatcher = new Agent({ connect: { lookup: ssrfSafeLookup } });
1532
+ } catch (e) {
1533
+ logger.warn(`[Scrapers] undici unavailable; connect-time SSRF pinning disabled (request-time validation still active): ${e instanceof Error ? e.message : String(e)}`);
1534
+ cachedSsrfDispatcher = null;
1535
+ }
1536
+ return cachedSsrfDispatcher;
1537
+ }
1538
+ function isPrivateIp(ip) {
1539
+ const parts = ip.split(".").map(Number);
1540
+ if (parts.length !== 4 || parts.some((p) => isNaN(p))) return false;
1541
+ const [a, b] = parts;
1542
+ if (a === void 0 || b === void 0) return false;
1543
+ return a === 0 || a === 10 || a === 127 || a === 100 && b >= 64 && b <= 127 || a === 169 && b === 254 || a === 172 && b >= 16 && b <= 31 || a === 192 && b === 168 || a >= 224;
1544
+ }
1545
+ function isPrivateIpv6(ip) {
1546
+ const normalized = ip.toLowerCase().replace(/^\[|\]$/g, "");
1547
+ if (normalized === "::1") return true;
1548
+ if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
1549
+ if (normalized.startsWith("fe8") || normalized.startsWith("fe9") || normalized.startsWith("fea") || normalized.startsWith("feb")) return true;
1550
+ const mapped = extractMappedIpv4(normalized);
1551
+ if (mapped) {
1552
+ return isPrivateIp(mapped);
1553
+ }
1554
+ if (normalized.startsWith("2002:")) return true;
1555
+ if (/^2001:0{0,4}:/.test(normalized) || normalized.startsWith("2001::")) return true;
1556
+ if (normalized === "::" || normalized === "0:0:0:0:0:0:0:0") return true;
1557
+ return false;
1558
+ }
1559
+ function extractMappedIpv4(ip) {
1560
+ const prefix = /^(?:::ffff:|(?:0:){5}ffff:)/i;
1561
+ if (!prefix.test(ip)) return null;
1562
+ const tail = ip.replace(prefix, "");
1563
+ if (/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(tail)) return tail;
1564
+ const hex = tail.match(/^([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i);
1565
+ if (hex && hex[1] && hex[2]) {
1566
+ const hi = parseInt(hex[1], 16);
1567
+ const lo = parseInt(hex[2], 16);
1568
+ return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1569
+ }
1570
+ return null;
1571
+ }
1572
+ function isMappedLoopback(ip) {
1573
+ const mapped = extractMappedIpv4(ip.toLowerCase().replace(/^\[|\]$/g, ""));
1574
+ return mapped !== null && /^127\./.test(mapped);
1575
+ }
1576
+ function stripImageLinks(markdown) {
1577
+ return markdown.replace(MARKDOWN_IMAGE_PATTERN, "").replace(IMAGE_LINK_PATTERN, "$1").replace(/\n{3,}/g, "\n\n").trim();
1578
+ }
1579
+ function createNativeMarkdownConverter(nativeModule) {
1580
+ return async (html) => {
1581
+ if (!nativeModule || !nativeModule.convert || !nativeModule.HeadingStyle || !nativeModule.CodeBlockStyle) {
1582
+ throw new Error("Native markdown converter not properly initialized");
1583
+ }
1584
+ const result = nativeModule.convert(html, {
1585
+ headingStyle: nativeModule.HeadingStyle.Atx,
1586
+ codeBlockStyle: nativeModule.CodeBlockStyle.Backticks,
1587
+ wrap: false
1588
+ });
1589
+ return stripImageLinks(result.content ?? "");
1590
+ };
1591
+ }
1592
+ function createJsMarkdownConverter() {
1593
+ const converter = new NodeHtmlMarkdown({
1594
+ codeBlockStyle: "fenced",
1595
+ textReplace: [[/\u00a0/g, " "]],
1596
+ ignore: [...FILTERED_TAGS]
1597
+ });
1598
+ return async (html) => {
1599
+ const markdown = converter.translate(html);
1600
+ return stripImageLinks(markdown);
1601
+ };
1602
+ }
1603
+ function validateContent(html, markdown, url) {
1604
+ const htmlLow = html.toLowerCase();
1605
+ for (const [pattern, reason] of BOT_PATTERNS) {
1606
+ if (htmlLow.includes(pattern.toLowerCase())) {
1607
+ metrics.increment("scrape_errors_total", 1, { error_type: "bot_protection" });
1608
+ const error = new Error(`Fetch blocked: ${reason}`);
1609
+ errorTracker.trackError(error, {
1610
+ component: "scrapers",
1611
+ operation: "validate",
1612
+ url,
1613
+ domain: extractDomain(url),
1614
+ errorType: "bot_protection"
1615
+ });
1616
+ throw error;
1617
+ }
1618
+ }
1619
+ const words = markdown.trim().split(/\s+/).filter((w) => w.length > 0);
1620
+ const charCount = markdown.replace(/\s+/g, "").length;
1621
+ let stubCheckHostname = "";
1622
+ try {
1623
+ stubCheckHostname = new URL(url).hostname;
1624
+ } catch {
1625
+ }
1626
+ if (words.length < 50 && charCount < 200 && stubCheckHostname !== "example.com") {
1627
+ metrics.increment("scrape_errors_total", 1, { error_type: "stub_content" });
1628
+ const error = new Error(`Fetch returned stub: only ${words.length} words / ${charCount} chars found.`);
1629
+ errorTracker.trackError(error, {
1630
+ component: "scrapers",
1631
+ operation: "validate",
1632
+ url,
1633
+ domain: extractDomain(url),
1634
+ errorType: "stub_content"
1635
+ });
1636
+ throw error;
1637
+ }
1638
+ }
1639
+ var cachedSsrfDispatcher;
1640
+ var init_scraper_utils = __esm({
1641
+ "src/web-research/scraper-utils.ts"() {
1642
+ "use strict";
1643
+ init_error_tracker();
1644
+ init_metrics();
1645
+ init_logger();
1646
+ init_scraper_types();
1647
+ init_browser_error_utils();
1648
+ cachedSsrfDispatcher = void 0;
1649
+ }
1650
+ });
1651
+
1652
+ // src/infrastructure/browser/thread-worker.ts
1653
+ import { ClusterWorker } from "poolifier";
1654
+ import crypto2 from "node:crypto";
1655
+ import process3 from "node:process";
1656
+
1657
+ // src/infrastructure/browser/thread-worker-lifecycle.ts
1658
+ init_log_utils();
1659
+ import process2 from "node:process";
1660
+ import cluster from "node:cluster";
1661
+ import * as fs from "node:fs/promises";
1662
+ var workerId = "";
1663
+ function setWorkerId(id) {
1664
+ workerId = id;
1665
+ }
1666
+ function setupIpcErrorHandler() {
1667
+ if (!cluster.isWorker || !cluster.worker) {
1668
+ return;
1669
+ }
1670
+ cluster.worker.on("error", (err) => {
1671
+ if (err && err.code === "ERR_IPC_CHANNEL_CLOSED") {
1672
+ return;
1673
+ }
1674
+ throw err;
1675
+ });
1676
+ }
1677
+ function setupUncaughtExceptionHandler() {
1678
+ process2.on("uncaughtException", (err) => {
1679
+ if (err && err.message && err.message.includes("reading 'url'") && err.stack && err.stack.includes("coreBundle.js")) {
1680
+ logToDebugFile("WARN", `[Worker-${workerId}] Suppressed Playwright core error: ${err.message}`);
1681
+ return;
1682
+ }
1683
+ if (err && err.message && err.message.includes("reading 'frames'") && err.stack && err.stack.includes("coreBundle.js")) {
1684
+ logToDebugFile("WARN", `[Worker-${workerId}] Suppressed Playwright frame error: ${err.message}`);
1685
+ return;
1686
+ }
1687
+ logToDebugFile("ERROR", `[Worker-${workerId}] Uncaught Exception: ${err.stack || err.message}`);
1688
+ process2.exit(1);
1689
+ });
1690
+ process2.on("unhandledRejection", (reason) => {
1691
+ const errMsg = reason instanceof Error ? reason.stack || reason.message : String(reason);
1692
+ const benignPlaywright = reason instanceof Error && /reading '(url|frames)'/.test(reason.message || "") && /coreBundle\.js/.test(reason.stack || "");
1693
+ logToDebugFile(
1694
+ benignPlaywright ? "DEBUG" : "WARN",
1695
+ `[Worker-${workerId}] Unhandled Rejection (non-fatal): ${errMsg}`
1696
+ );
1697
+ });
1698
+ }
1699
+ var orphanCheckTimer = null;
1700
+ var cleanupBrowser = null;
1701
+ function setBrowserCleanup(fn) {
1702
+ cleanupBrowser = fn;
1703
+ }
1704
+ function setupOrphanProtection() {
1705
+ if (!cluster.isWorker) {
1706
+ return;
1707
+ }
1708
+ if (!process2.ppid) {
1709
+ return;
1710
+ }
1711
+ const checkOrphan = async () => {
1712
+ try {
1713
+ process2.kill(process2.ppid, 0);
1714
+ orphanCheckTimer = setTimeout(checkOrphan, 1e4);
1715
+ if (orphanCheckTimer) {
1716
+ orphanCheckTimer.unref();
1717
+ }
1718
+ } catch (_e) {
1719
+ if (_e?.code === "EPERM") {
1720
+ orphanCheckTimer = setTimeout(checkOrphan, 1e4);
1721
+ orphanCheckTimer?.unref();
1722
+ return;
1723
+ }
1724
+ logToDebugFile("WARN", `[Worker-${workerId}] Parent process died or unreachable (orphaned), shutting down...`);
1725
+ if (cleanupBrowser) {
1726
+ await cleanupBrowser().catch((err) => {
1727
+ logToDebugFile("WARN", `[Worker-${workerId}] Browser cleanup failed during orphan exit:`, err);
1728
+ });
1729
+ }
1730
+ cleanupOrphanProtection();
1731
+ process2.env["PI_PROCESS_EXITING"] = "1";
1732
+ process2.exit(1);
1733
+ }
1734
+ };
1735
+ orphanCheckTimer = setTimeout(checkOrphan, 1e4);
1736
+ if (orphanCheckTimer) {
1737
+ orphanCheckTimer.unref();
1738
+ }
1739
+ }
1740
+ function cleanupOrphanProtection() {
1741
+ if (orphanCheckTimer) {
1742
+ clearTimeout(orphanCheckTimer);
1743
+ orphanCheckTimer = null;
1744
+ }
1745
+ }
1746
+ function logToDebugFile(level, ...args) {
1747
+ const logFile = process2.env["PI_RESEARCH_LOG_FILE"];
1748
+ if (!logFile) return;
1749
+ if ((level === "INFO" || level === "DEBUG") && process2.env["PI_RESEARCH_DEBUG"] !== "true") return;
1750
+ try {
1751
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1752
+ const entry = {
1753
+ timestamp,
1754
+ level,
1755
+ workerId,
1756
+ message: redactSecrets(args.map((arg) => {
1757
+ if (arg instanceof Error) return arg.stack || arg.message;
1758
+ if (typeof arg === "object" && arg !== null) return JSON.stringify(arg);
1759
+ return String(arg);
1760
+ }).join(" "))
1761
+ };
1762
+ fs.appendFile(logFile, `${JSON.stringify(entry)}
1763
+ `).catch(() => {
1764
+ });
1765
+ } catch {
1766
+ }
1767
+ }
1768
+ function createKillHandler() {
1769
+ return async () => {
1770
+ logToDebugFile("INFO", `[Worker-${workerId}] Worker shutting down`);
1771
+ if (cleanupBrowser) {
1772
+ await cleanupBrowser().catch((err) => {
1773
+ logToDebugFile("WARN", `[Worker-${workerId}] Browser cleanup failed during shutdown:`, err);
1774
+ });
1775
+ }
1776
+ cleanupOrphanProtection();
1777
+ if (!cluster.isWorker) {
1778
+ return;
1779
+ }
1780
+ setTimeout(() => {
1781
+ logToDebugFile("INFO", `[Worker-${workerId}] Forcing process exit (Cluster)`);
1782
+ process2.env["PI_PROCESS_EXITING"] = "1";
1783
+ process2.exit(0);
1784
+ }, 100).unref();
1785
+ };
1786
+ }
1787
+
1788
+ // src/infrastructure/browser/thread-worker-browser.ts
1789
+ import { platform as platform2 } from "node:os";
1790
+
1791
+ // src/infrastructure/browser/thread-worker-messaging.ts
1792
+ init_log_utils();
1793
+ import { appendFileSync as appendFileSync2 } from "node:fs";
1794
+ var workerId2 = "";
1795
+ var pageCreationLock = Promise.resolve();
1796
+ async function createPageSafe(context2) {
1797
+ let release;
1798
+ const nextLock = new Promise((resolve) => {
1799
+ release = resolve;
1800
+ });
1801
+ const currentLock = pageCreationLock;
1802
+ pageCreationLock = currentLock.then(() => nextLock);
1803
+ await currentLock;
1804
+ try {
1805
+ const pagePromise = context2.newPage();
1806
+ pagePromise.catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Background page creation rejection: ${err.message}`));
1807
+ let timeoutId;
1808
+ try {
1809
+ const result = await Promise.race([
1810
+ pagePromise,
1811
+ new Promise((_, reject) => {
1812
+ timeoutId = setTimeout(() => reject(new Error("Browser page creation timed out after 60000ms")), 6e4);
1813
+ })
1814
+ ]);
1815
+ if (timeoutId) clearTimeout(timeoutId);
1816
+ return result;
1817
+ } catch (err) {
1818
+ if (timeoutId) clearTimeout(timeoutId);
1819
+ pagePromise.then((p) => {
1820
+ try {
1821
+ p?.close?.();
1822
+ } catch {
1823
+ }
1824
+ }).catch(() => {
1825
+ });
1826
+ throw err;
1827
+ }
1828
+ } finally {
1829
+ release();
1830
+ }
1831
+ }
1832
+ function setWorkerId2(id) {
1833
+ workerId2 = id;
1834
+ }
1835
+ function logToDebugFile2(level, ...args) {
1836
+ const logFile = process.env["PI_RESEARCH_LOG_FILE"];
1837
+ if (!logFile) return;
1838
+ if ((level === "INFO" || level === "DEBUG") && process.env["PI_RESEARCH_DEBUG"] !== "true") return;
1839
+ try {
1840
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
1841
+ const entry = {
1842
+ timestamp,
1843
+ level,
1844
+ workerId: workerId2,
1845
+ // Redact secrets + bound size; worker logs include full scrape/search
1846
+ // URLs, which can carry credentials in userinfo or query strings.
1847
+ message: redactSecrets(args.map((arg) => {
1848
+ if (arg instanceof Error) return arg.stack || arg.message;
1849
+ if (typeof arg === "object" && arg !== null) return JSON.stringify(arg);
1850
+ return String(arg);
1851
+ }).join(" "))
1852
+ };
1853
+ appendFileSync2(logFile, `${JSON.stringify(entry)}
1854
+ `);
1855
+ } catch {
1856
+ }
1857
+ }
1858
+ async function setupMocking(context2) {
1859
+ const mockSearch = process.env["PI_RESEARCH_MOCK_SEARCH"] === "true";
1860
+ const mockScrape = process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true";
1861
+ if (!mockSearch && !mockScrape) return;
1862
+ logToDebugFile2("INFO", `[Worker-${workerId2}] Setting up browser mocking (Search: ${mockSearch}, Scrape: ${mockScrape})`);
1863
+ await context2.route("**", (route, request) => {
1864
+ const urlStr = request.url();
1865
+ let url;
1866
+ try {
1867
+ url = new URL(urlStr);
1868
+ } catch {
1869
+ route.continue();
1870
+ return;
1871
+ }
1872
+ const isDuckDuckGo = url.hostname === "duckduckgo.com" || url.hostname === "lite.duckduckgo.com" || url.hostname.endsWith(".duckduckgo.com");
1873
+ if (mockSearch && isDuckDuckGo) {
1874
+ logToDebugFile2("DEBUG", `[Worker-${workerId2}] Mocking search response for: ${urlStr}`);
1875
+ route.fulfill({
1876
+ status: 200,
1877
+ contentType: "text/html",
1878
+ body: `
1879
+ <html><head><title>DuckDuckGo Lite</title></head><body>
1880
+ <form action="/lite/" method="post">
1881
+ <input name="q" type="text">
1882
+ <input type="submit" value="Search">
1883
+ </form>
1884
+ <table class="result-link">
1885
+ <tr>
1886
+ <td><a class="result-link" href="https://example.com/mock-result-1?uddg=https%3A%2F%2Fexample.com%2Ftarget-1">Mock Result 1</a></td>
1887
+ </tr>
1888
+ <tr>
1889
+ <td class="result-snippet">This is a mocked search result snippet for testing purposes.</td>
1890
+ </tr>
1891
+ <tr>
1892
+ <td><a class="result-link" href="https://example.com/mock-result-2?uddg=https%3A%2F%2Fexample.com%2Ftarget-2">Mock Result 2</a></td>
1893
+ </tr>
1894
+ <tr>
1895
+ <td class="result-snippet">Another mocked snippet to verify multiple result extraction.</td>
1896
+ </tr>
1897
+ </table>
1898
+ </body></html>
1899
+ `
1900
+ });
1901
+ return;
1902
+ }
1903
+ if (mockScrape && !isDuckDuckGo) {
1904
+ logToDebugFile2("DEBUG", `[Worker-${workerId2}] Mocking scrape response for: ${urlStr}`);
1905
+ route.fulfill({
1906
+ status: 200,
1907
+ contentType: "text/html",
1908
+ body: `<html><body><h1>Mocked Page Content</h1><p>This content was mocked for ${url} during integration testing.</p></body></html>`
1909
+ });
1910
+ return;
1911
+ }
1912
+ route.continue();
1913
+ });
1914
+ }
1915
+ async function extractSearchResults(page) {
1916
+ return await page.evaluate(() => {
1917
+ const found = [];
1918
+ const links = Array.from(document.querySelectorAll("a.result-link"));
1919
+ links.forEach((link) => {
1920
+ const row = link.closest("tr");
1921
+ const snippet = row?.nextElementSibling?.querySelector("td.result-snippet")?.textContent?.trim() || "";
1922
+ const title = link.textContent?.trim() || "";
1923
+ let url = link.href;
1924
+ try {
1925
+ const u = new URL(url);
1926
+ const uddg = u.searchParams.get("uddg");
1927
+ if (uddg) url = decodeURIComponent(uddg || "");
1928
+ } catch {
1929
+ }
1930
+ let urlHostname = "";
1931
+ try {
1932
+ urlHostname = new URL(url).hostname;
1933
+ } catch {
1934
+ }
1935
+ if (title && url && urlHostname !== "duckduckgo.com" && !urlHostname.endsWith(".duckduckgo.com") && url.startsWith("http")) {
1936
+ found.push({ title, url, content: snippet });
1937
+ }
1938
+ });
1939
+ return found;
1940
+ });
1941
+ }
1942
+ async function executeSearchTask(_context, query) {
1943
+ const page = await createPageSafe(_context);
1944
+ const SEARCH_TIMEOUT = parseInt(process.env["PI_RESEARCH_SEARCH_TIMEOUT_MS"] || "45000", 10);
1945
+ page.setDefaultTimeout(SEARCH_TIMEOUT);
1946
+ page.setDefaultNavigationTimeout(SEARCH_TIMEOUT);
1947
+ try {
1948
+ logToDebugFile2("DEBUG", `[Worker-${workerId2}] Starting search for: ${query}`);
1949
+ if (process.env["PI_RESEARCH_MOCK_SEARCH"] === "true") {
1950
+ await page.goto(`https://lite.duckduckgo.com/lite/?q=${encodeURIComponent(query)}`, { waitUntil: "domcontentloaded" });
1951
+ } else {
1952
+ await page.goto("https://lite.duckduckgo.com/lite/", { waitUntil: "domcontentloaded" });
1953
+ await page.fill('input[name="q"]', query);
1954
+ await Promise.all([
1955
+ page.waitForNavigation({ waitUntil: "domcontentloaded" }),
1956
+ page.keyboard.press("Enter")
1957
+ ]);
1958
+ }
1959
+ const results = await extractSearchResults(page);
1960
+ await page.close();
1961
+ const jitterBase = process.env["PI_RESEARCH_MOCK_SEARCH"] === "true" ? 10 : 500;
1962
+ const jitterRange = process.env["PI_RESEARCH_MOCK_SEARCH"] === "true" ? 10 : 1e3;
1963
+ const jitter = Math.floor(Math.random() * jitterRange) + jitterBase;
1964
+ await new Promise((r) => setTimeout(r, jitter));
1965
+ return { results, jitter };
1966
+ } catch (error) {
1967
+ await page.close().catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Swallowed page close/wait error: ${err.message || String(err)}`));
1968
+ throw error;
1969
+ }
1970
+ }
1971
+ var SUBRESOURCE_DNS_VERDICT_TTL_MS = 15e3;
1972
+ var SUBRESOURCE_DNS_VERDICT_MAX_ENTRIES = 256;
1973
+ var subresourceDnsVerdicts = /* @__PURE__ */ new Map();
1974
+ function getCachedSubresourceVerdict(reqUrl, validate) {
1975
+ let hostname;
1976
+ try {
1977
+ hostname = new URL(reqUrl).hostname.toLowerCase();
1978
+ } catch (e) {
1979
+ return Promise.resolve(e instanceof Error ? e : new Error(String(e)));
1980
+ }
1981
+ const now = Date.now();
1982
+ const cached = subresourceDnsVerdicts.get(hostname);
1983
+ if (cached && cached.expires > now) return cached.verdict;
1984
+ const verdict = validate(reqUrl).then(
1985
+ () => null,
1986
+ (e) => e instanceof Error ? e : new Error(String(e))
1987
+ );
1988
+ if (subresourceDnsVerdicts.size >= SUBRESOURCE_DNS_VERDICT_MAX_ENTRIES) {
1989
+ for (const [key, entry] of subresourceDnsVerdicts) {
1990
+ if (entry.expires <= now) subresourceDnsVerdicts.delete(key);
1991
+ }
1992
+ while (subresourceDnsVerdicts.size >= SUBRESOURCE_DNS_VERDICT_MAX_ENTRIES) {
1993
+ const oldest = subresourceDnsVerdicts.keys().next().value;
1994
+ if (oldest === void 0) break;
1995
+ subresourceDnsVerdicts.delete(oldest);
1996
+ }
1997
+ }
1998
+ subresourceDnsVerdicts.set(hostname, { expires: now + SUBRESOURCE_DNS_VERDICT_TTL_MS, verdict });
1999
+ return verdict;
2000
+ }
2001
+ function seedSubresourceVerdict(validatedUrl) {
2002
+ try {
2003
+ const hostname = new URL(validatedUrl).hostname.toLowerCase();
2004
+ subresourceDnsVerdicts.set(hostname, {
2005
+ expires: Date.now() + SUBRESOURCE_DNS_VERDICT_TTL_MS,
2006
+ verdict: Promise.resolve(null)
2007
+ });
2008
+ } catch {
2009
+ }
2010
+ }
2011
+ async function executeScrapeTask(_context, url, signal) {
2012
+ const page = await createPageSafe(_context);
2013
+ const SCRAPE_TIMEOUT = parseInt(process.env["PI_RESEARCH_SCRAPE_TIMEOUT_MS"] || "15000", 10);
2014
+ page.setDefaultTimeout(SCRAPE_TIMEOUT);
2015
+ page.setDefaultNavigationTimeout(SCRAPE_TIMEOUT);
2016
+ const onAbort = () => {
2017
+ page.close().catch(() => {
2018
+ });
2019
+ };
2020
+ if (signal) {
2021
+ if (signal.aborted) onAbort();
2022
+ else signal.addEventListener("abort", onAbort, { once: true });
2023
+ }
2024
+ let poisonedError = null;
2025
+ const pendingAddrChecks = [];
2026
+ try {
2027
+ logToDebugFile2("DEBUG", `[Worker-${workerId2}] Starting scrape for: ${url}`);
2028
+ const { validateUrlForSSRF: validateForWorker, validateUrlForSSRFSync: validateForWorkerSync } = await Promise.resolve().then(() => (init_scraper_utils(), scraper_utils_exports));
2029
+ await page.route("**", async (route, req) => {
2030
+ const reqUrl = req.url();
2031
+ if (/^https?:/i.test(reqUrl)) {
2032
+ try {
2033
+ if (req.isNavigationRequest()) {
2034
+ await validateForWorker(reqUrl);
2035
+ seedSubresourceVerdict(reqUrl);
2036
+ } else {
2037
+ const loopbackAccepted = validateForWorkerSync(reqUrl);
2038
+ if (!loopbackAccepted) {
2039
+ const verdictError = await getCachedSubresourceVerdict(reqUrl, validateForWorker);
2040
+ if (verdictError) throw verdictError;
2041
+ }
2042
+ }
2043
+ } catch (_ssrfErr) {
2044
+ await route.abort("blockedbyclient").catch(() => {
2045
+ });
2046
+ return;
2047
+ }
2048
+ }
2049
+ await route.fallback().catch(() => {
2050
+ });
2051
+ });
2052
+ let finalMainFrameAddr = Promise.resolve(null);
2053
+ page.on("response", (resp) => {
2054
+ try {
2055
+ if (resp.request().resourceType() === "document" && resp.frame() === page.mainFrame()) {
2056
+ finalMainFrameAddr = resp.serverAddr().catch(() => null);
2057
+ }
2058
+ const check = Promise.resolve(resp.serverAddr()).then((addr) => {
2059
+ const ip = addr?.ipAddress;
2060
+ if (!ip || poisonedError) return;
2061
+ try {
2062
+ validateForWorkerSync(ip.includes(":") ? `http://[${ip}]/` : `http://${ip}/`);
2063
+ } catch (cause) {
2064
+ poisonedError = new Error(
2065
+ "Fetch blocked: a response was served from a private/reserved address (SSRF DNS-rebinding defense)",
2066
+ { cause }
2067
+ );
2068
+ logToDebugFile2("WARN", `[Worker-${workerId2}] SSRF rebinding defense: a response connected to a private/reserved address; poisoning scrape of ${url}`);
2069
+ page.close().catch(() => {
2070
+ });
2071
+ }
2072
+ }).catch(() => {
2073
+ });
2074
+ pendingAddrChecks.push(check);
2075
+ } catch {
2076
+ }
2077
+ });
2078
+ const response = await page.goto(url, { waitUntil: "domcontentloaded" });
2079
+ const serverAddr = await response?.serverAddr().catch(() => null);
2080
+ if (serverAddr?.ipAddress) {
2081
+ const ip = serverAddr.ipAddress;
2082
+ validateForWorkerSync(ip.includes(":") ? `http://[${ip}]/` : `http://${ip}/`);
2083
+ }
2084
+ const contentType = await response?.headerValue("content-type") || "";
2085
+ if (contentType.includes("application/pdf")) {
2086
+ if (!response) throw new Error(`[Worker] No response received for PDF URL: ${url}`);
2087
+ const buffer = await response.body();
2088
+ await Promise.allSettled(pendingAddrChecks);
2089
+ if (poisonedError) throw poisonedError;
2090
+ await page.close();
2091
+ return { contentType, buffer, jitter: 0 };
2092
+ }
2093
+ let html = await page.content();
2094
+ const cfPatterns = ["_cf_chl_", "cdn-cgi/challenge-platform", "cf_chl_opt", "Just a moment...", "Checking your browser before accessing"];
2095
+ const hasCloudflare = cfPatterns.some((pattern) => html.includes(pattern));
2096
+ if (hasCloudflare) {
2097
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Cloudflare challenge detected for: ${url}`);
2098
+ try {
2099
+ await page.waitForFunction(
2100
+ () => {
2101
+ const body = document.body.innerHTML;
2102
+ return !body.includes("_cf_chl_") && !body.includes("cdn-cgi/challenge-platform") && !body.includes("cf_chl_opt") && !body.includes("Just a moment...") && !body.includes("Checking your browser before accessing") && body.length > 200;
2103
+ },
2104
+ { timeout: 5e3 }
2105
+ );
2106
+ html = await page.content();
2107
+ logToDebugFile2("INFO", `[Worker-${workerId2}] Cloudflare challenge resolved for: ${url}`);
2108
+ } catch (_waitError) {
2109
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Cloudflare challenge failed for: ${url}`);
2110
+ const error = new Error("Fetch blocked: Cloudflare challenge");
2111
+ error.cause = _waitError;
2112
+ throw error;
2113
+ }
2114
+ }
2115
+ const needsWait = html.length < 5e3 || html.includes('id="root"') || html.includes('id="app"') || html.includes("<noscript>");
2116
+ if (needsWait) {
2117
+ await page.waitForLoadState("networkidle", { timeout: 1e4 }).catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Swallowed page close/wait error: ${err.message || String(err)}`));
2118
+ html = await page.content();
2119
+ }
2120
+ const jitter = process.env["PI_RESEARCH_MOCK_SCRAPE"] === "true" ? 0 : Math.floor(Math.random() * 1e3) + 500;
2121
+ if (jitter > 0) await new Promise((r) => setTimeout(r, jitter));
2122
+ const finalAddr = await finalMainFrameAddr;
2123
+ if (finalAddr?.ipAddress) {
2124
+ const fip = finalAddr.ipAddress;
2125
+ validateForWorkerSync(fip.includes(":") ? `http://[${fip}]/` : `http://${fip}/`);
2126
+ }
2127
+ await Promise.allSettled(pendingAddrChecks);
2128
+ if (poisonedError) throw poisonedError;
2129
+ await page.close();
2130
+ return { contentType, html, jitter };
2131
+ } catch (error) {
2132
+ await page.close().catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Swallowed page close/wait error: ${err.message || String(err)}`));
2133
+ if (poisonedError) throw poisonedError;
2134
+ throw error;
2135
+ } finally {
2136
+ signal?.removeEventListener("abort", onAbort);
2137
+ }
2138
+ }
2139
+ var HEALTH_PRIMARY_URL = "https://lite.duckduckgo.com/lite/";
2140
+ var HEALTH_FALLBACK_URL = "https://example.com/";
2141
+ async function executeHealthCheckAttempt(_context, navTimeoutMs, url = HEALTH_PRIMARY_URL) {
2142
+ const page = await createPageSafe(_context);
2143
+ page.setDefaultTimeout(navTimeoutMs);
2144
+ page.setDefaultNavigationTimeout(navTimeoutMs);
2145
+ try {
2146
+ const navStart = Date.now();
2147
+ await page.goto(url, { waitUntil: "domcontentloaded" });
2148
+ const navMs = Date.now() - navStart;
2149
+ if (navMs > 3e3) {
2150
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Health navigation to ${url} was slow: ${navMs}ms (expected <1s). Possible rate-limit or network congestion.`);
2151
+ }
2152
+ const title = await page.title();
2153
+ await page.close();
2154
+ return { success: !!title, navMs };
2155
+ } catch (error) {
2156
+ await page.close().catch((err) => logToDebugFile2("DEBUG", `[ThreadWorker] Swallowed page close/wait error: ${err.message || String(err)}`));
2157
+ throw error;
2158
+ }
2159
+ }
2160
+ async function executeHealthCheck(_context, initMs) {
2161
+ const configuredMs = parseInt(process.env["PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS"] || "0", 10);
2162
+ const HEALTH_TIMEOUT = configuredMs > 0 ? Math.max(1e4, configuredMs) : 1e4;
2163
+ if (initMs > 3e3) {
2164
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Browser init was slow: ${initMs}ms \u2014 leaving less headroom for DDG navigation.`);
2165
+ }
2166
+ try {
2167
+ return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT);
2168
+ } catch (firstError) {
2169
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Health check attempt 1 failed: ${firstError instanceof Error ? firstError.message : String(firstError)}. Retrying once...`);
2170
+ try {
2171
+ return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT);
2172
+ } catch (retryError) {
2173
+ const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
2174
+ const isTimeout = /tim(e|ed)\s*out|timeout/i.test(retryMsg);
2175
+ if (!isTimeout) {
2176
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Search-provider health probe failed twice (${retryMsg}); verifying open-web reachability via fallback endpoint...`);
2177
+ try {
2178
+ const fallback = await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_FALLBACK_URL);
2179
+ logToDebugFile2("WARN", `[Worker-${workerId2}] Fallback endpoint reachable \u2014 open web is up, search provider is degraded. Proceeding with research.`);
2180
+ return fallback;
2181
+ } catch (fallbackError) {
2182
+ logToDebugFile2("ERROR", `[Worker-${workerId2}] Health check failed after retry + fallback probe: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`);
2183
+ throw retryError;
2184
+ }
2185
+ }
2186
+ logToDebugFile2("ERROR", `[Worker-${workerId2}] Health check failed after retry: ${retryMsg}`);
2187
+ throw retryError;
2188
+ }
2189
+ }
2190
+ }
2191
+ function shouldResetBrowser(errorMsg) {
2192
+ return errorMsg.includes("Target closed") || errorMsg.includes("browser has disconnected") || errorMsg.includes("Protocol error") || errorMsg.includes("Session closed");
2193
+ }
2194
+
2195
+ // src/infrastructure/browser/thread-worker-browser.ts
2196
+ init_log_utils();
2197
+
2198
+ // src/infrastructure/browser/config.ts
2199
+ import { platform, homedir } from "node:os";
2200
+
2201
+ // src/config.ts
2202
+ init_logger();
2203
+ import { Type } from "typebox";
2204
+ import { Value } from "typebox/value";
2205
+
2206
+ // src/utils/text-utils.ts
2207
+ init_logger();
2208
+
2209
+ // src/utils/url-utils.ts
2210
+ init_logger();
2211
+
2212
+ // src/config.ts
2213
+ var ConfigSchema = Type.Object({
2214
+ /** Per-researcher timeout in milliseconds (default: 300000, range: 3-30 min) */
2215
+ RESEARCHER_TIMEOUT_MS: Type.Number({ minimum: 18e4, maximum: 18e5, default: 3e5 }),
2216
+ /** Maximum number of concurrent researcher processes (default: 3, range: 1-5) */
2217
+ MAX_CONCURRENT_RESEARCHERS: Type.Number({ minimum: 1, maximum: 5, default: 3 }),
2218
+ /** Maximum number of retries for a failed researcher (default: 2, range: 0-5) */
2219
+ RESEARCHER_MAX_RETRIES: Type.Number({ minimum: 0, maximum: 5, default: 2 }),
2220
+ /** Base delay between retries in milliseconds (default: 2000, range: 100-10000) */
2221
+ RESEARCHER_MAX_RETRY_DELAY_MS: Type.Number({ minimum: 100, maximum: 1e4, default: 2e3 }),
2222
+ /** Target depth for recursive research (default: 1, range: 1-3) */
2223
+ DEFAULT_RESEARCH_DEPTH: Type.Number({ minimum: 1, maximum: 3, default: 1 }),
2224
+ /** Number of batches to allow for a single scrape tool call (default: 2, 0=unlimited) */
2225
+ MAX_SCRAPE_BATCHES: Type.Number({ minimum: 0, maximum: 99, default: 2 }),
2226
+ /** Max shared web-gathering calls per researcher (search + security_search + stackexchange + youtube_transcript). Default 12. */
2227
+ MAX_GATHERING_CALLS: Type.Number({ minimum: 1, maximum: 100, default: 12 }),
2228
+ /** Number of parallel browser pool workers (default: 4, range: 1-10) */
2229
+ WORKER_THREADS: Type.Number({ minimum: 1, maximum: 10, default: 4 }),
2230
+ /** Number of concurrent tasks per pool worker process (default: 2, range: 1-10) */
2231
+ WORKER_CONCURRENCY: Type.Number({ minimum: 1, maximum: 10, default: 2 }),
2232
+ /** Knowledge store isolation mode (default: 'global' — one shared store across every directory) */
2233
+ KNOWLEDGE_STORE_MODE: Type.Union([Type.Literal("none"), Type.Literal("project"), Type.Literal("global")], { default: "global" }),
2234
+ /** Embedding model to use for the knowledge store */
2235
+ EMBEDDING_MODEL: Type.String({ default: "onnx-community/granite-embedding-small-english-r2-ONNX" }),
2236
+ /** Hardware backend for embeddings: 'auto' (probe WebGPU viability out-of-process, fall back to CPU), 'webgpu' (force), or 'cpu' (force).
2237
+ * Default 'auto': many real targets (VMs, containers, CI, headless/software-Vulkan hosts) have no GPU that
2238
+ * onnxruntime-node's bundled Dawn can run compute on — and that failure is a native SIGSEGV, not a catchable
2239
+ * JS error. 'auto' detects this via a disposable child process (a crash there can't kill us) and uses CPU. */
2240
+ EMBEDDING_DEVICE: Type.Union([Type.Literal("auto"), Type.Literal("webgpu"), Type.Literal("cpu")], { default: "auto" }),
2241
+ /** Timeout for scraping operations in milliseconds (default: 15000, range: 5-120 seconds) */
2242
+ SCRAPE_TIMEOUT_MS: Type.Number({ minimum: 5e3, maximum: 12e4, default: 15e3 }),
2243
+ /** How long to keep documents in the knowledge store before eviction (default: 30 days) */
2244
+ KNOWLEDGE_STORE_CACHE_TTL_DAYS: Type.Number({ minimum: 1, maximum: 365, default: 30 }),
2245
+ /** Max age (days) a cached document may be served at read time; 0 (default) disables the
2246
+ * read-time gate — eviction (KNOWLEDGE_STORE_CACHE_TTL_DAYS) still bounds the worst case.
2247
+ * Set >0 for time-sensitive research to force a fresh scrape of anything older than this. */
2248
+ KNOWLEDGE_STORE_MAX_SERVE_AGE_DAYS: Type.Number({ minimum: 0, maximum: 3650, default: 0 }),
2249
+ /** Timeout for embedding model initialization (default: 300000ms) */
2250
+ EMBEDDING_MODEL_INIT_TIMEOUT_MS: Type.Number({ minimum: 1e4, maximum: 6e5, default: 3e5 }),
2251
+ /** Max fraction of context window to use for initial scrape context (default: 0.15) */
2252
+ MAX_SCRAPE_TOKEN_FRACTION_FOR_SCRAPING: Type.Number({ minimum: 0.05, maximum: 1, default: 0.15 }),
2253
+ /** Estimated tokens per scrape result for planning (default: 2500) */
2254
+ AVG_TOKENS_PER_SCRAPE: Type.Number({ minimum: 500, maximum: 1e4, default: 2500 }),
2255
+ /** Maximum number of concurrent scrapes (default: 3) */
2256
+ MAX_CONCURRENT_SCRAPES: Type.Number({ minimum: 1, maximum: 20, default: 3 }),
2257
+ /** Max YouTube videos transcribed per youtube_transcript call (default: 3, range 1-5) */
2258
+ YOUTUBE_TRANSCRIPT_MAX_VIDEOS: Type.Number({ minimum: 1, maximum: 5, default: 3 }),
2259
+ /** Per-video timeout for the youtube_transcript tool in ms (default: 20000, range 5-120s) */
2260
+ YOUTUBE_TRANSCRIPT_TIMEOUT_MS: Type.Number({ minimum: 5e3, maximum: 12e4, default: 2e4 }),
2261
+ /** Preferred caption language (BCP-47 prefix) for transcripts (default: 'en') */
2262
+ YOUTUBE_TRANSCRIPT_LANG: Type.String({ default: "en" }),
2263
+ /** Append 'youtube' to roughly one-in-N search queries for video discovery
2264
+ * (default: 5; 1 = every query). Configured via env/config file; not surfaced
2265
+ * in the config TUI. */
2266
+ YOUTUBE_QUERY_EVERY_N: Type.Number({ minimum: 1, maximum: 100, default: 5 }),
2267
+ /** Health check timeout in milliseconds (default: 10000ms) */
2268
+ HEALTH_CHECK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
2269
+ /** Default timeout for browser page operations like search (default: 45000ms) */
2270
+ SEARCH_TIMEOUT_MS: Type.Number({ minimum: 5e3, maximum: 12e4, default: 45e3 }),
2271
+ /** TUI refresh debounce in milliseconds (default: 100ms) */
2272
+ TUI_REFRESH_DEBOUNCE_MS: Type.Number({ minimum: 0, maximum: 1e3, default: 100 }),
2273
+ /** Queue-wait / overhead margin (ms) added on top of each browser operation's own nav
2274
+ * timeout to form its scheduler task-timeout ceiling: a search task times out after
2275
+ * SEARCH_TIMEOUT_MS + this, a scrape after SCRAPE_TIMEOUT_MS + this. Sized so a task is
2276
+ * never killed before it has used its full nav budget plus time waiting in the queue.
2277
+ * (default: 10000) */
2278
+ BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
2279
+ /** Timeout for coordinator/evaluator/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-600s).
2280
+ * Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var. */
2281
+ LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 6e5, default: 3e5 }),
2282
+ /** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
2283
+ * evaluator, synthesis, JSON-repair, knowledge extraction) AND the researcher
2284
+ * sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
2285
+ * open-ended reasoning, so thinking only burns the output-token budget (often
2286
+ * truncating before the JSON/report block) and adds latency for at most a marginal
2287
+ * quality gain that does not justify the cost on these structured calls.
2288
+ * Deliberately NOT exposed in the /research-config TUI — an advanced knob set only
2289
+ * via the PI_RESEARCH_LLM_THINKING_LEVEL env var. 'off' maps to the provider's
2290
+ * thinking-disabled request (e.g. z.ai thinking:{type:'disabled'}). */
2291
+ LLM_THINKING_LEVEL: Type.Union([
2292
+ Type.Literal("off"),
2293
+ Type.Literal("minimal"),
2294
+ Type.Literal("low"),
2295
+ Type.Literal("medium"),
2296
+ Type.Literal("high")
2297
+ ], { default: "off" }),
2298
+ /** Max output tokens for the coordinator plan + mid-round evaluator decision (and,
2299
+ * when the evaluator synthesizes mid-round, the report). Default 16384. The blanket
2300
+ * 4096 cap previously throttled every call and could be exhausted by a thinking block
2301
+ * before any text was emitted. Capped at the model's real ceiling at call time.
2302
+ * Not exposed in TUI — controlled via PI_RESEARCH_PLANNING_MAX_TOKENS env var. */
2303
+ PLANNING_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 16384 }),
2304
+ /** Max output tokens for the final synthesized research report (the forced-synthesis
2305
+ * evaluator call). Default 32768 so a full cited report is never truncated. Capped at
2306
+ * the model's real maxTokens at call time.
2307
+ * Not exposed in TUI — controlled via PI_RESEARCH_SYNTHESIS_MAX_TOKENS env var. */
2308
+ SYNTHESIS_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 32768 }),
2309
+ /** LLM Model override for researcher sub-agents and knowledge synthesis.
2310
+ * Format: provider/model-id (e.g. google/gemini-2.0-flash-001) or just model-id.
2311
+ * When set, this overrides ctx.model for researcher sub-agents (both deep and quick)
2312
+ * and the knowledge synthesis background LLM. The coordinator and evaluator always
2313
+ * use the caller's model (ctx.model).
2314
+ */
2315
+ RESEARCH_MODEL: Type.Optional(Type.String()),
2316
+ /** Explicit directory for the knowledge store database (overrides default) */
2317
+ KNOWLEDGE_STORE_DIR: Type.Optional(Type.String()),
2318
+ /** Directory for transient browser profile data. Defaults to a disk-backed
2319
+ * dir (~/.cache/pi-research/profiles) rather than the system temp dir, so a
2320
+ * RAM-backed /tmp (tmpfs) is not consumed by per-worker browser profiles.
2321
+ * Set to a path under the system temp dir to opt back into tmpfs/RAM. */
2322
+ TMP_DIR: Type.Optional(Type.String()),
2323
+ /** Whether to automatically export a markdown research report to disk at the end (default: false) */
2324
+ RESEARCH_REPORT_EXPORT_ENABLED: Type.Boolean({ default: false }),
2325
+ /**
2326
+ * Explicit directory for exported reports. When set, reports are written here
2327
+ * verbatim (created if needed), bypassing the cwd-relative "smart" resolution.
2328
+ * Lets the skill pin a stable location instead of writing into
2329
+ * whatever repo the host agent happens to be in. Empty = smart cwd resolution.
2330
+ */
2331
+ RESEARCH_REPORT_EXPORT_DIR: Type.Optional(Type.String()),
2332
+ /** Strategy for database schema/model migrations: 'drop', 're-embed', or 'backup' (default: 'backup') */
2333
+ MIGRATION_STRATEGY: Type.Union([
2334
+ Type.Literal("drop"),
2335
+ Type.Literal("re-embed"),
2336
+ Type.Literal("backup")
2337
+ ], { default: "backup" }),
2338
+ /** Whether to mirror logs to the console (stdout/stderr). (default: false) */
2339
+ CONSOLE_LOG: Type.Boolean({ default: false }),
2340
+ /** Enable debug/verbose logging (writes INFO+DEBUG to log file). (default: false) */
2341
+ DEBUG: Type.Boolean({ default: false }),
2342
+ /**
2343
+ * Comma-separated list of research tools to DISABLE for every researcher in a run
2344
+ * (e.g. "scrape", or "youtube_transcript,stackexchange"). Valid tool names:
2345
+ * search, scrape, security_search, stackexchange, youtube_transcript, grep.
2346
+ * Disable-only: the list is merged into the per-run excludeTools stream, so disabled
2347
+ * tools are both removed from each researcher's toolset AND named in the
2348
+ * coordinator/evaluator "DISABLED TOOLS" prompt section. Env-only
2349
+ * (PI_RESEARCH_DISABLED_TOOLS); intentionally NOT exposed in the config TUI.
2350
+ * Empty/absent = all tools enabled.
2351
+ */
2352
+ DISABLED_TOOLS: Type.Optional(Type.String())
2353
+ });
2354
+ var DEFAULTS = Value.Create(ConfigSchema);
2355
+
2356
+ // src/infrastructure/browser/config.ts
2357
+ init_logger();
2358
+ function resolveHeadlessMode() {
2359
+ const osPlatform = platform();
2360
+ if (osPlatform !== "linux") return true;
2361
+ if (process.env["DISPLAY"]) return true;
2362
+ if (process.env["WAYLAND_DISPLAY"]) return true;
2363
+ if (process.env["PI_RESEARCH_USE_XVFB"] === "true") return "virtual";
2364
+ return true;
2365
+ }
2366
+
2367
+ // src/infrastructure/browser/thread-worker-browser.ts
2368
+ var browser = null;
2369
+ var context = null;
2370
+ var initPromise = null;
2371
+ var workerId3 = "";
2372
+ function setWorkerId3(id) {
2373
+ workerId3 = id;
2374
+ }
2375
+ var isBrowserConnected = () => {
2376
+ try {
2377
+ return browser && typeof browser.isConnected === "function" && browser.isConnected();
2378
+ } catch {
2379
+ logToDebugFile3("DEBUG", "[ThreadWorker] browser.isConnected check threw, treating as disconnected");
2380
+ return false;
2381
+ }
2382
+ };
2383
+ function logToDebugFile3(level, ...args) {
2384
+ const logFile = process.env["PI_RESEARCH_LOG_FILE"];
2385
+ if (!logFile) return;
2386
+ if ((level === "INFO" || level === "DEBUG") && process.env["PI_RESEARCH_DEBUG"] !== "true") return;
2387
+ try {
2388
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
2389
+ const entry = {
2390
+ timestamp,
2391
+ level,
2392
+ workerId: workerId3,
2393
+ message: redactSecrets(args.map((arg) => {
2394
+ if (arg instanceof Error) return arg.stack || arg.message;
2395
+ if (typeof arg === "object" && arg !== null) return JSON.stringify(arg);
2396
+ return String(arg);
2397
+ }).join(" "))
2398
+ };
2399
+ import("node:fs/promises").then((fsp) => fsp.appendFile(logFile, `${JSON.stringify(entry)}
2400
+ `)).catch(() => {
2401
+ });
2402
+ } catch {
2403
+ }
2404
+ }
2405
+ async function initBrowser() {
2406
+ if (isBrowserConnected() && context) return;
2407
+ if (initPromise) return initPromise;
2408
+ initPromise = (async () => {
2409
+ try {
2410
+ if (!isBrowserConnected() || !context) {
2411
+ logToDebugFile3("INFO", `[Worker-${workerId3}] Initializing browser instance...`);
2412
+ let CamoufoxModule;
2413
+ try {
2414
+ CamoufoxModule = await import("camoufox-js");
2415
+ } catch (e) {
2416
+ throw new Error(`[Worker] camoufox-js not found in node_modules. Please run 'npm install'. Original error: ${e instanceof Error ? e.message : String(e)}`, { cause: e });
2417
+ }
2418
+ const { Camoufox } = CamoufoxModule;
2419
+ const launchTimeoutMs = 9e4;
2420
+ const launchOnce = async (headless) => {
2421
+ const launchPromise = Camoufox({
2422
+ headless,
2423
+ humanize: true,
2424
+ locale: "en-US",
2425
+ screen: {
2426
+ width: 1920,
2427
+ height: 1080,
2428
+ colorDepth: 24,
2429
+ pixelRatio: 1
2430
+ }
2431
+ });
2432
+ launchPromise.catch((err) => logToDebugFile3("DEBUG", `[ThreadWorker] Background browser launch rejection: ${err.message}`));
2433
+ let launchTimeoutId;
2434
+ try {
2435
+ return await Promise.race([
2436
+ launchPromise,
2437
+ new Promise((_, reject) => {
2438
+ launchTimeoutId = setTimeout(() => reject(new Error(`Browser launch timed out after ${launchTimeoutMs}ms`)), launchTimeoutMs);
2439
+ })
2440
+ ]);
2441
+ } finally {
2442
+ if (launchTimeoutId) clearTimeout(launchTimeoutId);
2443
+ }
2444
+ };
2445
+ const primaryHeadless = resolveHeadlessMode();
2446
+ let launchedBrowser;
2447
+ try {
2448
+ launchedBrowser = await launchOnce(primaryHeadless);
2449
+ } catch (launchErr) {
2450
+ const lmsg = launchErr instanceof Error ? launchErr.message : String(launchErr);
2451
+ const wasTimeout = lmsg.includes("timed out");
2452
+ if (platform2() === "win32" && primaryHeadless === true && !wasTimeout) {
2453
+ logToDebugFile3("WARN", `[Worker-${workerId3}] Headless browser launch failed on Windows (${lmsg}); falling back to a visible window so scraping still works (camoufox issue #614).`);
2454
+ launchedBrowser = await launchOnce(false);
2455
+ } else {
2456
+ throw launchErr;
2457
+ }
2458
+ }
2459
+ browser = launchedBrowser;
2460
+ const contextPromise = browser.newContext();
2461
+ contextPromise.catch((err) => logToDebugFile3("DEBUG", `[ThreadWorker] Background browser context rejection: ${err.message}`));
2462
+ let contextTimeoutId;
2463
+ context = await Promise.race([
2464
+ contextPromise,
2465
+ new Promise((_, reject) => {
2466
+ contextTimeoutId = setTimeout(() => reject(new Error("Browser context creation timed out after 30000ms")), 3e4);
2467
+ })
2468
+ ]);
2469
+ if (contextTimeoutId) clearTimeout(contextTimeoutId);
2470
+ await setupMocking(context);
2471
+ logToDebugFile3("INFO", `[Worker-${workerId3}] Browser initialized.`);
2472
+ }
2473
+ } catch (e) {
2474
+ if (browser && typeof browser.close === "function") {
2475
+ Promise.resolve(browser.close()).catch((err) => logToDebugFile3("DEBUG", `[Worker-${workerId3}] Swallowed browser close error during failed init: ${err.message}`));
2476
+ }
2477
+ browser = null;
2478
+ context = null;
2479
+ const msg = e instanceof Error ? e.message : String(e);
2480
+ if (msg.includes("Camoufox is not installed") || msg.includes("Version information not found")) {
2481
+ throw new Error(`[Worker] Browser binaries not found. Please run 'npx camoufox-js fetch' to install them.`, { cause: e });
2482
+ }
2483
+ const errName = e instanceof Error ? e.constructor?.name ?? "" : "";
2484
+ if (errName === "CannotFindXvfb" || errName === "CannotExecuteXvfb" || msg.includes("Xvfb") || msg.includes("virtual display")) {
2485
+ throw new Error(
2486
+ "[Worker] No display server found on Linux. Install Xvfb for headless use (TTY/Wayland): sudo apt install xvfb",
2487
+ { cause: e }
2488
+ );
2489
+ }
2490
+ throw e;
2491
+ } finally {
2492
+ initPromise = null;
2493
+ }
2494
+ })();
2495
+ return initPromise;
2496
+ }
2497
+ function getContext() {
2498
+ return context;
2499
+ }
2500
+ function resetBrowser() {
2501
+ if (context) Promise.resolve(context.close()).catch((err) => logToDebugFile3("DEBUG", `[Worker-${workerId3}] Swallowed context close error during reset: ${err.message}`));
2502
+ if (browser) Promise.resolve(browser.close()).catch((err) => logToDebugFile3("DEBUG", `[Worker-${workerId3}] Swallowed browser close error during reset: ${err.message}`));
2503
+ context = null;
2504
+ browser = null;
2505
+ }
2506
+ async function cleanupBrowser2() {
2507
+ const timeoutMs = 2e3;
2508
+ const cleanup = async () => {
2509
+ if (context) await Promise.resolve(context.close()).catch((err) => logToDebugFile3("DEBUG", `[Worker-${workerId3}] Swallowed context close error during cleanup: ${err.message}`));
2510
+ if (browser) await Promise.resolve(browser.close()).catch((err) => logToDebugFile3("DEBUG", `[Worker-${workerId3}] Swallowed browser close error during cleanup: ${err.message}`));
2511
+ };
2512
+ try {
2513
+ await Promise.race([
2514
+ cleanup(),
2515
+ new Promise((_, reject) => setTimeout(() => reject(new Error("Browser cleanup timed out")), timeoutMs))
2516
+ ]);
2517
+ } catch {
2518
+ } finally {
2519
+ context = null;
2520
+ browser = null;
2521
+ }
2522
+ }
2523
+
2524
+ // src/infrastructure/browser/thread-worker.ts
2525
+ var workerId4 = crypto2.randomBytes(2).toString("hex");
2526
+ setWorkerId(workerId4);
2527
+ setWorkerId3(workerId4);
2528
+ setWorkerId2(workerId4);
2529
+ setupIpcErrorHandler();
2530
+ setupUncaughtExceptionHandler();
2531
+ setBrowserCleanup(cleanupBrowser2);
2532
+ if (process3.env["GITHUB_ACTIONS"] !== "true") {
2533
+ setupOrphanProtection();
2534
+ }
2535
+ var FULL_MOCK_MODE = process3.env["PI_RESEARCH_MOCK_SEARCH"] === "true" && process3.env["PI_RESEARCH_MOCK_SCRAPE"] === "true";
2536
+ async function runTask(data) {
2537
+ if (!data) {
2538
+ return { error: "No task data provided", duration: 0 };
2539
+ }
2540
+ const { type, query, url, queuedAt: _queuedAt, taskTimeoutMs } = data;
2541
+ const startTime = Date.now();
2542
+ if (FULL_MOCK_MODE) {
2543
+ if (type === "search") {
2544
+ return {
2545
+ results: [{ title: "Mock Result", url: "https://example.com/mock", content: `Mock search result for: ${query ?? ""}` }],
2546
+ duration: Date.now() - startTime,
2547
+ jitter: 0
2548
+ };
2549
+ }
2550
+ if (type === "scrape") {
2551
+ return {
2552
+ contentType: "text/html",
2553
+ html: `<html><body><p>Mock content for ${url ?? ""}</p></body></html>`,
2554
+ duration: Date.now() - startTime,
2555
+ jitter: 0
2556
+ };
2557
+ }
2558
+ if (type === "healthcheck") {
2559
+ return { success: true, navMs: 0, duration: Date.now() - startTime };
2560
+ }
2561
+ }
2562
+ const abortController = new AbortController();
2563
+ let taskTimer;
2564
+ if (taskTimeoutMs && taskTimeoutMs > 0) {
2565
+ taskTimer = setTimeout(() => abortController.abort(new Error(`Task timed out after ${taskTimeoutMs}ms`)), taskTimeoutMs);
2566
+ }
2567
+ try {
2568
+ await initBrowser();
2569
+ const initMs = Date.now() - startTime;
2570
+ let result;
2571
+ if (type === "search") {
2572
+ if (!query) throw new Error("Search task requires a query");
2573
+ const searchResult = await executeSearchTask(getContext(), query);
2574
+ result = { results: searchResult.results, duration: Date.now() - startTime, jitter: searchResult.jitter };
2575
+ } else if (type === "scrape") {
2576
+ if (!url) throw new Error("Scrape task requires a URL");
2577
+ const scrapeResult = await executeScrapeTask(getContext(), url, abortController.signal);
2578
+ result = { ...scrapeResult, duration: Date.now() - startTime };
2579
+ } else if (type === "healthcheck") {
2580
+ const healthResult = await executeHealthCheck(getContext(), initMs);
2581
+ result = { ...healthResult, duration: Date.now() - startTime };
2582
+ } else {
2583
+ result = { error: "Unknown task type", duration: Date.now() - startTime };
2584
+ }
2585
+ if (abortController.signal.aborted) {
2586
+ return { error: `Task timed out after ${taskTimeoutMs}ms`, duration: Date.now() - startTime };
2587
+ }
2588
+ return result;
2589
+ } catch (error) {
2590
+ const errMsg = error instanceof Error ? error.message : String(error);
2591
+ if (abortController.signal.aborted) {
2592
+ return { error: `Task timed out after ${taskTimeoutMs}ms`, duration: Date.now() - startTime };
2593
+ }
2594
+ if (shouldResetBrowser(errMsg)) {
2595
+ resetBrowser();
2596
+ }
2597
+ return {
2598
+ error: errMsg,
2599
+ duration: Date.now() - startTime
2600
+ };
2601
+ } finally {
2602
+ if (taskTimer !== void 0) clearTimeout(taskTimer);
2603
+ }
2604
+ }
2605
+ var worker = new ClusterWorker(runTask, {
2606
+ killHandler: createKillHandler()
2607
+ });
2608
+ var thread_worker_default = worker;
2609
+ export {
2610
+ thread_worker_default as default
2611
+ };