@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,323 @@
1
+ /**
2
+ * Thread Worker Lifecycle Management
3
+ *
4
+ * Handles worker lifecycle - orphaned worker protection, shutdown handling,
5
+ * and worker process management.
6
+ */
7
+
8
+ import process from 'node:process';
9
+ import cluster from 'node:cluster';
10
+ import * as fs from 'node:fs/promises';
11
+ import { redactSecrets } from '../../utils/log-utils.ts';
12
+
13
+ let workerId: string = '';
14
+
15
+ /**
16
+ * Set the worker ID for logging purposes
17
+ */
18
+ export function setWorkerId(id: string): void {
19
+ workerId = id;
20
+ }
21
+
22
+ /**
23
+ * Handle ERR_IPC_CHANNEL_CLOSED when poolifier tries to send messages during shutdown
24
+ */
25
+ export function setupIpcErrorHandler(): void {
26
+ if (!cluster.isWorker || !cluster.worker) {
27
+ return;
28
+ }
29
+ cluster.worker.on('error', (err: any) => {
30
+ if (err && err.code === 'ERR_IPC_CHANNEL_CLOSED') {
31
+ return;
32
+ }
33
+ throw err;
34
+ });
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Master-side counterpart to setupIpcErrorHandler()
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * IPC error codes that are benign when a worker's channel is mid-teardown: the
43
+ * write failed because the pipe is already gone. poolifier independently counts
44
+ * worker errors and respawns, so swallowing these does not hide a real fault.
45
+ */
46
+ const BENIGN_IPC_CODES = new Set(['EPIPE', 'ERR_IPC_CHANNEL_CLOSED', 'ECONNRESET']);
47
+
48
+ export function isBenignClusterIpcError(err: any): boolean {
49
+ if (!err) return false;
50
+ if (typeof err.code === 'string' && BENIGN_IPC_CODES.has(err.code)) return true;
51
+ const msg = String(err.message || err);
52
+ return /\b(EPIPE|ECONNRESET)\b/.test(msg) || /channel closed/i.test(msg);
53
+ }
54
+
55
+ let masterIpcGuardInstalled = false;
56
+
57
+ /**
58
+ * Master-side guard against the unhandled `write EPIPE` that crashed the whole pi
59
+ * host (observed twice on 2026-06-30 during a populate loop, each preceded by
60
+ * knowledge-extraction 429s → worker churn).
61
+ *
62
+ * Mechanism: poolifier's FixedClusterPool dispatches tasks with cluster
63
+ * `worker.send()`. When a worker's IPC channel closes (a 429-driven respawn, a
64
+ * crash, or terminate()), an in-flight/subsequent send completes with `write
65
+ * EPIPE`. Node's cluster module re-emits that as an 'error' on the cluster.Worker.
66
+ * poolifier attaches its own 'error' listener — but during terminate() it calls
67
+ * `worker.removeAllListeners()`, after which the cluster module's internal re-emit
68
+ * (`worker.process.on('error', ...)`, which poolifier never strips) fires
69
+ * `worker.emit('error', EPIPE)` on a Worker with NO listeners → EventEmitter
70
+ * throws → uncaughtException → the pi host exits.
71
+ *
72
+ * The worker-side guard (setupIpcErrorHandler) returns early in the master, so the
73
+ * master had no equivalent. This installs a PERSISTENT benign-IPC-error swallower
74
+ * on every forked worker that survives poolifier's removeAllListeners(), so the
75
+ * cluster.Worker is never without an 'error' listener at the moment cluster
76
+ * re-emits. Idempotent; safe to call before each pool (re)creation.
77
+ */
78
+ export function setupMasterIpcErrorHandler(): void {
79
+ if (masterIpcGuardInstalled) return;
80
+ // Workers use setupIpcErrorHandler(); this guard is for the primary process
81
+ // that owns the cluster pool and originates the failing worker.send() calls.
82
+ if (cluster.isWorker) return;
83
+ masterIpcGuardInstalled = true;
84
+
85
+ // cluster emits 'fork' for every worker poolifier creates, including the
86
+ // respawns it triggers via startMinimumNumberOfWorkers() after an unexpected
87
+ // exit — so this catches the churn-driven workers, not just the initial set.
88
+ cluster.on('fork', (worker) => guardClusterWorker(worker));
89
+
90
+ // Cover any workers already forked before this handler was installed.
91
+ for (const id of Object.keys(cluster.workers ?? {})) {
92
+ const w = (cluster.workers as any)?.[id];
93
+ if (w) guardClusterWorker(w);
94
+ }
95
+ }
96
+
97
+ /** Reset for tests — re-arms the one-time install guard. */
98
+ export function __resetMasterIpcGuardForTests(): void {
99
+ masterIpcGuardInstalled = false;
100
+ }
101
+
102
+ function guardClusterWorker(worker: any): void {
103
+ if (!worker || worker.__piIpcGuarded) return;
104
+ worker.__piIpcGuarded = true;
105
+
106
+ const guard = (err: any) => {
107
+ if (isBenignClusterIpcError(err)) {
108
+ logToDebugFile('DEBUG', `[Master] Swallowed benign worker IPC error: ${err?.code || err?.message}`);
109
+ return;
110
+ }
111
+ // Non-benign worker errors are independently handled by poolifier's own
112
+ // errorHandler (which counts them and triggers pool recovery). Re-throwing
113
+ // here would itself become an uncaughtException, so just record it.
114
+ logToDebugFile('WARN', `[Master] Worker error (non-IPC): ${err?.stack || err?.message || err}`);
115
+ };
116
+
117
+ worker.on('error', guard);
118
+ if (worker.process && typeof worker.process.on === 'function') {
119
+ // The channel-write EPIPE is emitted on the underlying ChildProcess; cluster
120
+ // re-emits it to the Worker, but listen here too so a direct process 'error'
121
+ // (no re-emit) is also handled.
122
+ worker.process.on('error', guard);
123
+ }
124
+
125
+ // poolifier calls worker.removeAllListeners() during terminate(), stripping our
126
+ // guard right before the channel's final write can EPIPE. Re-arm after any
127
+ // removal so the cluster.Worker is never without an 'error' listener.
128
+ if (typeof worker.removeAllListeners === 'function') {
129
+ const originalRemoveAll = worker.removeAllListeners.bind(worker);
130
+ worker.removeAllListeners = (event?: string | symbol) => {
131
+ const ret = originalRemoveAll(event);
132
+ if (event === undefined || event === 'error') {
133
+ worker.on('error', guard);
134
+ }
135
+ return ret;
136
+ };
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Handle Uncaught Exceptions in Worker
142
+ */
143
+ export function setupUncaughtExceptionHandler(): void {
144
+ process.on('uncaughtException', (err: Error) => {
145
+ // Suppress Playwright core errors that shouldn't crash the worker
146
+ if (err && err.message && err.message.includes('reading \'url\'') && err.stack && err.stack.includes('coreBundle.js')) {
147
+ logToDebugFile('WARN', `[Worker-${workerId}] Suppressed Playwright core error: ${err.message}`);
148
+ return;
149
+ }
150
+ // Also suppress Playwright frame errors (reading 'frames')
151
+ if (err && err.message && err.message.includes('reading \'frames\'') && err.stack && err.stack.includes('coreBundle.js')) {
152
+ logToDebugFile('WARN', `[Worker-${workerId}] Suppressed Playwright frame error: ${err.message}`);
153
+ return;
154
+ }
155
+
156
+ logToDebugFile('ERROR', `[Worker-${workerId}] Uncaught Exception: ${err.stack || err.message}`);
157
+ // A non-suppressed uncaughtException leaves the worker's V8 state undefined
158
+ // (Node's documented semantics). Continuing to serve scrape/search tasks from
159
+ // here risks corrupt results, so exit and let poolifier respawn a clean worker.
160
+ // The pool's exitHandler counts non-zero exits and auto-recovers after a few,
161
+ // so a genuinely crash-looping worker is still handled — unlike a silently
162
+ // broken one. The two Playwright-internal patterns above are returned before
163
+ // this point precisely because they do NOT corrupt worker state.
164
+ process.exit(1);
165
+ });
166
+
167
+ process.on('unhandledRejection', (reason: unknown) => {
168
+ const errMsg = reason instanceof Error ? (reason.stack || reason.message) : String(reason);
169
+ // Log but do NOT exit — a stray Playwright background rejection (e.g. cancelled
170
+ // navigation) would kill an otherwise healthy worker and cause pool churn.
171
+ // Poolifier's own health-check will replace the worker if it becomes truly broken.
172
+ //
173
+ // Known-benign Playwright-internal rejection: dispatching a scraped page's own
174
+ // uncaught JS error throws inside coreBundle.js when Firefox reports a `pageError`
175
+ // with no `location` (`reading 'url'`) or a torn-down frame (`reading 'frames'`).
176
+ // It originates in Playwright's dispatcher before any of our listeners, so it
177
+ // cannot be prevented at the source — and it has zero effect on the scrape result.
178
+ // Downgrade to DEBUG so it doesn't masquerade as an actionable WARN in the logs.
179
+ const benignPlaywright = reason instanceof Error
180
+ && /reading '(url|frames)'/.test(reason.message || '')
181
+ && /coreBundle\.js/.test(reason.stack || '');
182
+ logToDebugFile(
183
+ benignPlaywright ? 'DEBUG' : 'WARN',
184
+ `[Worker-${workerId}] Unhandled Rejection (non-fatal): ${errMsg}`,
185
+ );
186
+ });
187
+ }
188
+
189
+ // Orphaned worker protection: If parent dies, kill the worker.
190
+ // This works cross-platform (Linux, Mac, Windows) in Node.js.
191
+ let orphanCheckTimer: NodeJS.Timeout | null = null;
192
+ let cleanupBrowser: (() => Promise<void>) | null = null;
193
+
194
+ /**
195
+ * Set the browser cleanup function to call on shutdown
196
+ */
197
+ export function setBrowserCleanup(fn: () => Promise<void>): void {
198
+ cleanupBrowser = fn;
199
+ }
200
+
201
+ /**
202
+ * Set up orphaned worker protection - shuts down the worker if parent process dies
203
+ */
204
+ export function setupOrphanProtection(): void {
205
+ if (!cluster.isWorker) {
206
+ return;
207
+ }
208
+
209
+ if (!process.ppid) {
210
+ return;
211
+ }
212
+
213
+ const checkOrphan = async () => {
214
+ try {
215
+ // signal 0 checks if the process is alive
216
+ process.kill(process.ppid, 0);
217
+
218
+ // Schedule next check
219
+ orphanCheckTimer = setTimeout(checkOrphan, 10000);
220
+ if (orphanCheckTimer) {
221
+ orphanCheckTimer.unref();
222
+ }
223
+ } catch (_e) {
224
+ // EPERM means the parent is ALIVE but owned by another user (multi-user host
225
+ // / CI) — NOT orphaned. Only ESRCH (no such process) is a real orphan. Don't
226
+ // self-terminate a healthy worker (and tear down its live browser) on EPERM.
227
+ if ((_e as NodeJS.ErrnoException)?.code === 'EPERM') {
228
+ orphanCheckTimer = setTimeout(checkOrphan, 10000);
229
+ orphanCheckTimer?.unref();
230
+ return;
231
+ }
232
+ // If error is thrown, the parent process is likely dead or unreachable
233
+ logToDebugFile('WARN', `[Worker-${workerId}] Parent process died or unreachable (orphaned), shutting down...`);
234
+ // FIX: Await cleanup to prevent browser/context leaks
235
+ if (cleanupBrowser) {
236
+ await cleanupBrowser().catch(err => {
237
+ logToDebugFile('WARN', `[Worker-${workerId}] Browser cleanup failed during orphan exit:`, err);
238
+ });
239
+ }
240
+ // Clear the orphan check timer
241
+ cleanupOrphanProtection();
242
+ process.env['PI_PROCESS_EXITING'] = '1';
243
+ process.exit(1);
244
+ }
245
+ };
246
+
247
+ // Initial schedule
248
+ orphanCheckTimer = setTimeout(checkOrphan, 10000);
249
+ if (orphanCheckTimer) {
250
+ orphanCheckTimer.unref();
251
+ }
252
+ }
253
+
254
+ /**
255
+ * Clean up orphan protection resources
256
+ */
257
+ export function cleanupOrphanProtection(): void {
258
+ if (orphanCheckTimer) {
259
+ clearTimeout(orphanCheckTimer);
260
+ orphanCheckTimer = null;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Log to debug file
266
+ */
267
+ function logToDebugFile(level: string, ...args: any[]): void {
268
+ const logFile = process.env['PI_RESEARCH_LOG_FILE'];
269
+ if (!logFile) return;
270
+ // Match the main-process logger: WARN/ERROR are always recorded (crash diagnostics);
271
+ // INFO/DEBUG only when verbose. Without this gate a non-verbose scaling run still
272
+ // streamed thousands of worker DEBUG lines to PI_RESEARCH_LOG_FILE — often a tmpfs
273
+ // (RAM) path — adding memory and I/O pressure for no benefit during sustained loops.
274
+ if ((level === 'INFO' || level === 'DEBUG') && process.env['PI_RESEARCH_DEBUG'] !== 'true') return;
275
+
276
+ try {
277
+ const timestamp = new Date().toISOString();
278
+ const entry = {
279
+ timestamp,
280
+ level,
281
+ workerId,
282
+ message: redactSecrets(args.map(arg => {
283
+ if (arg instanceof Error) return arg.stack || arg.message;
284
+ if (typeof arg === 'object' && arg !== null) return JSON.stringify(arg);
285
+ return String(arg);
286
+ }).join(' '))
287
+ };
288
+ // FIX (#32): Use async fs.appendFile to avoid blocking the event loop.
289
+ // Fire-and-forget is acceptable for debug logging in error handlers.
290
+ fs.appendFile(logFile, `${JSON.stringify(entry)}\n`).catch(() => {});
291
+ } catch {
292
+ // ignore
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Create a kill handler for poolifier worker shutdown
298
+ */
299
+ export function createKillHandler(): () => Promise<void> {
300
+ return async () => {
301
+ logToDebugFile('INFO', `[Worker-${workerId}] Worker shutting down`);
302
+ if (cleanupBrowser) {
303
+ await cleanupBrowser().catch(err => {
304
+ logToDebugFile('WARN', `[Worker-${workerId}] Browser cleanup failed during shutdown:`, err);
305
+ });
306
+ }
307
+ // Clear the orphan check timer to prevent it from keeping the event loop alive
308
+ cleanupOrphanProtection();
309
+
310
+ if (!cluster.isWorker) {
311
+ // Primary process — killHandler should not exit the orchestrator.
312
+ return;
313
+ }
314
+
315
+ // Force process exit for cluster workers — without this, the orphan-detection setInterval keeps
316
+ // the event loop alive indefinitely after the IPC channel is disconnected.
317
+ setTimeout(() => {
318
+ logToDebugFile('INFO', `[Worker-${workerId}] Forcing process exit (Cluster)`);
319
+ process.env['PI_PROCESS_EXITING'] = '1';
320
+ process.exit(0);
321
+ }, 100).unref();
322
+ };
323
+ }