@lincoln504/pi-research 0.1.13 → 1.0.1
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.
- package/LICENSE +1 -1
- package/README.md +55 -138
- package/dist/cli.mjs +23375 -0
- package/dist/prompts/research-knowledge-relevance-triage.md +35 -0
- package/dist/prompts/research-knowledge-search-extractor.md +41 -0
- package/dist/prompts/research-tool-usage.md +92 -0
- package/dist/prompts/researcher.md +89 -0
- package/dist/prompts/system-coordinator.md +75 -0
- package/dist/prompts/system-lead-evaluator.md +118 -0
- package/dist/thread-worker.mjs +2611 -0
- package/dist/webgpu-probe.mjs +80 -0
- package/docs/AGENT-SKILL.md +130 -0
- package/docs/ARCHITECTURE.md +306 -80
- package/docs/CONFIGURATION.md +249 -0
- package/docs/KNOWLEDGE-STORE.md +211 -0
- package/docs/PI-EXTENSION.md +91 -0
- package/docs/SDK.md +122 -0
- package/docs/THIRD-PARTY-NOTICES.md +66 -0
- package/package.json +102 -36
- package/scripts/build.cjs +140 -0
- package/scripts/cleanup.cjs +149 -0
- package/scripts/prepare.cjs +28 -0
- package/scripts/setup.cjs +134 -0
- package/skills/pi-research/README.md +90 -0
- package/skills/pi-research/SKILL.md +102 -0
- package/skills/pi-research/references/configuration.md +77 -0
- package/skills/pi-research/scripts/run.mjs +237 -0
- package/src/cleanup/research-cleanup.ts +140 -0
- package/src/cli.ts +1195 -0
- package/src/config.ts +1032 -155
- package/src/constants.ts +115 -84
- package/src/core/health-check-service.ts +265 -0
- package/src/core/interfaces/health-check-interfaces.ts +50 -0
- package/src/core/interfaces/knowledge-interfaces.ts +190 -0
- package/src/core/interfaces/observer-interfaces.ts +67 -0
- package/src/core/interfaces/orchestration-interfaces.ts +86 -0
- package/src/core/interfaces/planning-interfaces.ts +100 -0
- package/src/core/interfaces/process-interfaces.ts +37 -0
- package/src/core/interfaces/research-plan-types.ts +52 -0
- package/src/core/interfaces/scheduler-interfaces.ts +91 -0
- package/src/core/interfaces/service-names.ts +39 -0
- package/src/core/interfaces/state-manager-interfaces.ts +29 -0
- package/src/core/interfaces/steering-interfaces.ts +31 -0
- package/src/core/llm/agentic-repair.ts +169 -0
- package/src/core/llm/inject-date.ts +52 -0
- package/src/core/llm/llm-timeout.ts +64 -0
- package/src/core/llm/llm-utils.ts +86 -0
- package/src/core/llm/model-registry-factory.ts +287 -0
- package/src/core/llm/pi-ai-completion.ts +29 -0
- package/src/core/llm/prompts.ts +50 -0
- package/src/core/llm/research-model-resolver.ts +91 -0
- package/src/core/planning-constants.ts +13 -0
- package/src/core/planning-service.ts +649 -0
- package/src/core/planning-utils.ts +299 -0
- package/src/core/scheduler-factory.ts +48 -0
- package/src/core/scheduler-service.ts +123 -0
- package/src/core/service-initialization.ts +194 -0
- package/src/core/service-interfaces.ts +48 -0
- package/src/core/service-registry.ts +659 -0
- package/src/healthcheck/index.ts +271 -265
- package/src/healthcheck/registry.ts +130 -0
- package/src/index.ts +639 -50
- package/src/infrastructure/browser/browser-cleanup.ts +309 -0
- package/src/infrastructure/browser/browser-client.ts +199 -0
- package/src/infrastructure/browser/browser-error-utils.ts +160 -0
- package/src/infrastructure/browser/browser-lifecycle.ts +120 -0
- package/src/infrastructure/browser/browser-server.ts +174 -0
- package/src/infrastructure/browser/browser-task-scheduler.ts +461 -0
- package/src/infrastructure/browser/cleanup-utils.ts +155 -0
- package/src/infrastructure/browser/client-agent.ts +31 -0
- package/src/infrastructure/browser/config.ts +288 -0
- package/src/infrastructure/browser/ensure-browser.ts +205 -0
- package/src/infrastructure/browser/index.ts +37 -0
- package/src/infrastructure/browser/priority-task-queue.ts +226 -0
- package/src/infrastructure/browser/scheduler-factory.ts +354 -0
- package/src/infrastructure/browser/task-execution-service.ts +209 -0
- package/src/infrastructure/browser/thread-worker-browser.ts +282 -0
- package/src/infrastructure/browser/thread-worker-lifecycle.ts +323 -0
- package/src/infrastructure/browser/thread-worker-messaging.ts +662 -0
- package/src/infrastructure/browser/thread-worker-types.ts +25 -0
- package/src/infrastructure/browser/thread-worker.mjs +2611 -0
- package/src/infrastructure/browser/thread-worker.ts +168 -0
- package/src/infrastructure/browser/worker-pool-manager.ts +449 -0
- package/src/infrastructure/embedding/embedding-client.ts +205 -0
- package/src/infrastructure/embedding/embedding-factory.ts +324 -0
- package/src/infrastructure/embedding/embedding-server.ts +535 -0
- package/src/infrastructure/file-lock-service.ts +592 -0
- package/src/infrastructure/gpu-resource-service.ts +191 -0
- package/src/infrastructure/knowledge-store-service.ts +475 -0
- package/src/infrastructure/metrics-service.ts +129 -0
- package/src/infrastructure/process-lifecycle-service.ts +253 -0
- package/src/infrastructure/scheduler-factory-service.ts +58 -0
- package/src/infrastructure/service-initialization.ts +292 -0
- package/src/infrastructure/state/state-backup-manager.ts +290 -0
- package/src/infrastructure/state/state-browser-api.ts +62 -0
- package/src/infrastructure/state/state-browser-manager.ts +70 -0
- package/src/infrastructure/state/state-manager-service.ts +264 -0
- package/src/infrastructure/state/state-manager.ts +547 -0
- package/src/infrastructure/state/state-metrics.ts +92 -0
- package/src/infrastructure/state/state-migration.ts +103 -0
- package/src/infrastructure/state/state-path-configuration.ts +119 -0
- package/src/infrastructure/state/state-session-api.ts +94 -0
- package/src/infrastructure/state/state-session-manager.ts +127 -0
- package/src/infrastructure/state/state-validator.ts +93 -0
- package/src/infrastructure/types/state-types.ts +85 -0
- package/src/knowledge/chunker.ts +157 -0
- package/src/knowledge/embedder-init.ts +413 -0
- package/src/knowledge/embedder-utils.ts +296 -0
- package/src/knowledge/embedder.ts +592 -0
- package/src/knowledge/index.ts +126 -0
- package/src/knowledge/lancedb-loader.ts +35 -0
- package/src/knowledge/migration.ts +17 -0
- package/src/knowledge/model-config.ts +132 -0
- package/src/knowledge/onnx-env.ts +48 -0
- package/src/knowledge/store-operations.ts +291 -0
- package/src/knowledge/store-schema.ts +79 -0
- package/src/knowledge/store.ts +1251 -0
- package/src/knowledge/transformers-loader.ts +31 -0
- package/src/knowledge/webgpu-probe.mjs +80 -0
- package/src/knowledge/webgpu-viability.ts +255 -0
- package/src/knowledge/writer-queue.ts +252 -0
- package/src/logger.ts +316 -122
- package/src/observers/research-observer-impl.ts +459 -0
- package/src/orchestration/deep-research-orchestrator.ts +524 -773
- package/src/orchestration/deep-research-types.ts +47 -35
- package/src/orchestration/headless-observer.ts +114 -0
- package/src/orchestration/orchestration-types.ts +33 -0
- package/src/orchestration/quick-research-orchestrator.ts +463 -0
- package/src/orchestration/research-orchestration-service.ts +549 -0
- package/src/orchestration/research-session-service.ts +178 -0
- package/src/orchestration/research-synthesis-service.ts +348 -0
- package/src/orchestration/researcher-executor.ts +394 -0
- package/src/orchestration/researcher.ts +115 -42
- package/src/orchestration/service-initialization.ts +109 -0
- package/src/orchestration/session-context.ts +7 -3
- package/src/orchestration/session-state.ts +672 -0
- package/src/prompts/research-knowledge-relevance-triage.md +35 -0
- package/src/prompts/research-knowledge-search-extractor.md +41 -0
- package/src/prompts/research-tool-usage.md +63 -16
- package/src/prompts/researcher.md +59 -136
- package/src/prompts/system-coordinator.md +64 -13
- package/src/prompts/system-lead-evaluator.md +82 -189
- package/src/research-config.ts +872 -0
- package/src/sdk.ts +847 -0
- package/src/security/cisa-kev.ts +72 -9
- package/src/security/cvss.ts +92 -0
- package/src/security/ecosystem.ts +45 -0
- package/src/security/github-advisories.ts +168 -428
- package/src/security/github-advisory-types.ts +282 -0
- package/src/security/index.ts +63 -177
- package/src/security/nvd-client.ts +550 -0
- package/src/security/nvd-types.ts +179 -0
- package/src/security/osv-client.ts +246 -0
- package/src/security/osv-types.ts +285 -0
- package/src/security/types.ts +67 -53
- package/src/skill-install/skill-installer.ts +519 -0
- package/src/stackexchange/index.ts +81 -27
- package/src/stackexchange/output/compact.ts +7 -6
- package/src/stackexchange/output/table.ts +11 -7
- package/src/stackexchange/queries.ts +0 -10
- package/src/stackexchange/rest-client.ts +141 -66
- package/src/stackexchange/types.ts +0 -17
- package/src/tool.ts +15 -423
- package/src/tools/grep.ts +32 -310
- package/src/tools/health-tool-definition.ts +101 -0
- package/src/tools/index.ts +38 -9
- package/src/tools/research-knowledge-search.ts +974 -0
- package/src/tools/research-knowledge-types.ts +77 -0
- package/src/tools/research-tool-definition.ts +494 -0
- package/src/tools/scrape.ts +286 -388
- package/src/tools/search.ts +98 -153
- package/src/tools/security.ts +107 -80
- package/src/tools/stackexchange.ts +58 -35
- package/src/tools/youtube-transcript.ts +163 -0
- package/src/tui/knowledge-search-panel.ts +86 -0
- package/src/tui/pulse.ts +86 -0
- package/src/tui/research-health.ts +124 -0
- package/src/tui/research-panel-color-utils.ts +340 -0
- package/src/tui/research-panel-format.ts +54 -0
- package/src/tui/research-panel-state.ts +404 -0
- package/src/tui/research-panel-wave.ts +280 -0
- package/src/tui/research-panel.ts +339 -435
- package/src/tui/research-tui-manager.ts +233 -0
- package/src/tui/tui-controller.ts +137 -0
- package/src/types/extension-context.ts +18 -49
- package/src/types/index.ts +61 -0
- package/src/types/llm.ts +68 -29
- package/src/types/research-panel-types.ts +61 -0
- package/src/utils/circuit-breaker.ts +146 -0
- package/src/utils/citation-utils.ts +97 -0
- package/src/utils/disk-space-checker.ts +55 -0
- package/src/utils/error-tracker.ts +265 -0
- package/src/utils/host-config.ts +45 -0
- package/src/utils/input-validation.ts +8 -10
- package/src/utils/json-utils.ts +150 -16
- package/src/utils/llm-usage.ts +54 -0
- package/src/utils/log-rotation.ts +125 -0
- package/src/utils/log-utils.ts +159 -0
- package/src/utils/make-resource-loader.ts +3 -1
- package/src/utils/metrics-summary.ts +459 -0
- package/src/utils/metrics.ts +302 -0
- package/src/utils/research-export.ts +28 -12
- package/src/utils/safe-unref.ts +31 -0
- package/src/utils/shared-links.ts +195 -90
- package/src/utils/shutdown-manager.ts +65 -0
- package/src/utils/stdio-capture.ts +407 -0
- package/src/utils/text-utils.ts +258 -11
- package/src/utils/tool-usage-tracker.ts +83 -16
- package/src/utils/url-utils.ts +255 -0
- package/src/utils/user-agent.ts +28 -0
- package/src/web-research/browser-search.ts +179 -0
- package/src/web-research/retry-utils.ts +173 -77
- package/src/web-research/scraper-types.ts +72 -0
- package/src/web-research/scraper-utils.ts +434 -0
- package/src/web-research/search.ts +78 -218
- package/src/web-research/types.ts +9 -24
- package/src/web-research/utils.ts +2 -143
- package/src/web-research/web-scraper.ts +477 -0
- package/src/youtube/index.ts +154 -0
- package/src/youtube/potoken.ts +259 -0
- package/src/youtube/transcript-client.ts +390 -0
- package/src/youtube/video-id.ts +118 -0
- package/config/searxng/default-settings.yml +0 -189
- package/config/searxng/limiter.toml +0 -34
- package/config/tooling/eslint.config.js +0 -73
- package/config/tooling/vitest.config.integration.ts +0 -17
- package/config/tooling/vitest.config.ts +0 -40
- package/config/tooling/vitest.config.unit.ts +0 -12
- package/docs/assets/README-banner.jpg +0 -0
- package/scripts/setup.js +0 -212
- package/src/infrastructure/searxng-lifecycle.ts +0 -644
- package/src/infrastructure/searxng-manager.ts +0 -1183
- package/src/infrastructure/state-manager.ts +0 -918
- package/src/orchestration/deep-research-reducer.ts +0 -129
- package/src/orchestration/id-utils.ts +0 -63
- package/src/security/nvd.ts +0 -532
- package/src/security/osv.ts +0 -487
- package/src/stackexchange/cache.ts +0 -111
- package/src/stackexchange/output/index.ts +0 -7
- package/src/stackexchange/output/json.ts +0 -21
- package/src/tui/searxng-status.ts +0 -80
- package/src/utils/inject-date.ts +0 -29
- package/src/utils/research-error.ts +0 -205
- package/src/utils/searxng-config.ts +0 -94
- package/src/utils/session-state.ts +0 -295
- package/src/web-research/dependencies.ts +0 -20
- package/src/web-research/scrapers.ts +0 -935
- 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
|
+
}
|