@lincoln504/pi-research 1.3.11 → 1.4.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/README.md +2 -2
- package/dist/cli.mjs +5558 -4596
- package/dist/prompts/research-tool-usage.md +1 -1
- package/dist/prompts/researcher.md +1 -0
- package/dist/prompts/system-coordinator.md +1 -1
- package/dist/prompts/system-lead-router.md +122 -0
- package/dist/prompts/system-lead-synthesizer.md +46 -0
- package/dist/thread-worker.mjs +123 -30
- package/docs/ARCHITECTURE.md +69 -24
- package/docs/CHANGELOG.md +215 -0
- package/docs/CONFIGURATION.md +108 -8
- package/package.json +19 -11
- package/scripts/build.cjs +8 -1
- package/scripts/cleanup.cjs +26 -5
- package/scripts/setup.cjs +25 -1
- package/skills/pi-research/SKILL.md +1 -1
- package/src/cli.ts +41 -2
- package/src/config.ts +12 -8
- package/src/constants.ts +1 -1
- package/src/core/interfaces/observer-interfaces.ts +3 -3
- package/src/core/interfaces/orchestration-interfaces.ts +2 -0
- package/src/core/interfaces/planning-interfaces.ts +7 -1
- package/src/core/interfaces/scheduler-interfaces.ts +18 -2
- package/src/core/llm/agentic-repair.ts +7 -2
- package/src/core/llm/inject-date.ts +4 -1
- package/src/core/llm/llm-utils.ts +7 -0
- package/src/core/planning-service.ts +719 -91
- package/src/core/planning-utils.ts +21 -5
- package/src/core/service-initialization.ts +19 -3
- package/src/core/service-registry.ts +19 -3
- package/src/healthcheck/index.ts +11 -2
- package/src/index.ts +4 -3
- package/src/infrastructure/browser/browser-client.ts +68 -34
- package/src/infrastructure/browser/browser-task-scheduler.ts +220 -34
- package/src/infrastructure/browser/config.ts +77 -6
- package/src/infrastructure/browser/priority-task-queue.ts +55 -1
- package/src/infrastructure/browser/scheduler-factory.ts +12 -1
- package/src/infrastructure/browser/task-execution-service.ts +17 -7
- package/src/infrastructure/browser/thread-worker-browser.ts +66 -20
- package/src/infrastructure/browser/thread-worker-messaging.ts +38 -6
- package/src/infrastructure/browser/thread-worker.mjs +123 -30
- package/src/infrastructure/browser/thread-worker.ts +2 -2
- package/src/infrastructure/browser/worker-pool-manager.ts +127 -12
- package/src/infrastructure/embedding/embedding-client.ts +14 -4
- package/src/infrastructure/embedding/embedding-factory.ts +83 -15
- package/src/infrastructure/file-lock-service.ts +214 -19
- package/src/infrastructure/gpu-resource-service.ts +15 -1
- package/src/infrastructure/knowledge-store-service.ts +14 -0
- package/src/infrastructure/research-run-semaphore.ts +1 -1
- package/src/infrastructure/service-initialization.ts +7 -0
- package/src/knowledge/embedder-init.ts +60 -18
- package/src/knowledge/store.ts +11 -5
- package/src/logger.ts +11 -0
- package/src/orchestration/deep-research-orchestrator.ts +141 -87
- package/src/orchestration/quick-research-orchestrator.ts +11 -2
- package/src/orchestration/research-orchestration-service.ts +30 -1
- package/src/orchestration/research-synthesis-service.ts +53 -1
- package/src/orchestration/researcher-executor.ts +53 -2
- package/src/orchestration/researcher.ts +15 -2
- package/src/prompts/research-tool-usage.md +1 -1
- package/src/prompts/researcher.md +1 -0
- package/src/prompts/system-coordinator.md +1 -1
- package/src/prompts/system-lead-router.md +122 -0
- package/src/prompts/system-lead-synthesizer.md +46 -0
- package/src/sdk.ts +10 -1
- package/src/security/nvd-client.ts +55 -4
- package/src/security/osv-client.ts +13 -1
- package/src/tools/research-knowledge-search.ts +29 -4
- package/src/tools/scrape.ts +6 -0
- package/src/tools/search.ts +7 -0
- package/src/tools/security.ts +27 -9
- package/src/tools/stackexchange.ts +13 -7
- package/src/tools/youtube-transcript.ts +26 -12
- package/src/utils/cancellation.ts +36 -0
- package/src/utils/coverage-digest.ts +204 -0
- package/src/utils/llm-usage.ts +33 -6
- package/src/utils/log-utils.ts +16 -1
- package/src/utils/metrics-summary.ts +20 -4
- package/src/utils/metrics.ts +27 -1
- package/src/web-research/browser-search.ts +93 -20
- package/src/web-research/scraper-utils.ts +55 -14
- package/dist/prompts/system-lead-evaluator.md +0 -118
- package/src/prompts/system-lead-evaluator.md +0 -118
|
@@ -18,7 +18,7 @@ import { getServiceContainer, getService } from '../../core/service-registry.ts'
|
|
|
18
18
|
import type { ServiceContainer } from '../../core/service-registry.ts';
|
|
19
19
|
import { ServiceNames } from '../../core/service-interfaces.ts';
|
|
20
20
|
import type { ISchedulerFactory, IScheduler } from '../../core/scheduler-factory.ts';
|
|
21
|
-
import type { ISchedulerInternals } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
21
|
+
import type { ISchedulerInternals, TaskDispatchListener } from '../../core/interfaces/scheduler-interfaces.ts';
|
|
22
22
|
import type { IStateManager } from '../../core/interfaces/state-manager-interfaces.ts';
|
|
23
23
|
import { BrowserClient } from './browser-client.ts';
|
|
24
24
|
|
|
@@ -136,9 +136,19 @@ async function isRegisteredLeaderAlive(container: ServiceContainer): Promise<boo
|
|
|
136
136
|
return result;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
/**
|
|
140
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Test seam: drop BOTH module-level globals that survive between tests — the
|
|
141
|
+
* coalesced liveness probe (1s window) and the restart cooldown (10s window).
|
|
142
|
+
*
|
|
143
|
+
* Both are far longer than a test file's runtime, so a test that reaches
|
|
144
|
+
* restartSchedulerWithHerdGuard leaves state that silently short-circuits the
|
|
145
|
+
* next one: isRegisteredLeaderAliveUncached / forceSchedulerRestart simply are
|
|
146
|
+
* not re-invoked. Today nothing observes that only because the one test that
|
|
147
|
+
* reaches this path happens to run last — ordering, not a guarantee.
|
|
148
|
+
*/
|
|
149
|
+
export function _resetTaskExecutionGlobalsForTests(): void {
|
|
141
150
|
leaderLivenessProbe = null;
|
|
151
|
+
lastRestartTime = 0;
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
/**
|
|
@@ -321,7 +331,7 @@ export async function runBrowserHealthCheck(config?: Config, retries = 1, signal
|
|
|
321
331
|
/**
|
|
322
332
|
* Run a worker search query with retry logic.
|
|
323
333
|
*/
|
|
324
|
-
export async function runWorkerSearch(query: string, config?: Config, signal?: AbortSignal, retries = 1, sessionId?: string, container: ServiceContainer = getServiceContainer(), handoverRetries = LEADER_HANDOVER_RETRIES): Promise<SearchResult[]> {
|
|
334
|
+
export async function runWorkerSearch(query: string, config?: Config, signal?: AbortSignal, retries = 1, sessionId?: string, container: ServiceContainer = getServiceContainer(), handoverRetries = LEADER_HANDOVER_RETRIES, onTaskEvent?: TaskDispatchListener): Promise<SearchResult[]> {
|
|
325
335
|
if (signal?.aborted) throw new Error('Aborted');
|
|
326
336
|
|
|
327
337
|
const breaker = sessionId ? getBrowserCircuitBreaker(sessionId) : browserCircuitBreaker;
|
|
@@ -330,7 +340,7 @@ export async function runWorkerSearch(query: string, config?: Config, signal?: A
|
|
|
330
340
|
return await breaker.execute(async () => {
|
|
331
341
|
if (signal?.aborted) throw new Error('Aborted');
|
|
332
342
|
const scheduler = await getScheduler(config, container);
|
|
333
|
-
return await scheduler.runSearch(query, config, signal);
|
|
343
|
+
return await scheduler.runSearch(query, config, signal, onTaskEvent);
|
|
334
344
|
});
|
|
335
345
|
} catch (error: unknown) {
|
|
336
346
|
if (signal?.aborted || (error instanceof Error && error.message === 'Aborted')) throw new Error('Aborted', { cause: error });
|
|
@@ -348,7 +358,7 @@ export async function runWorkerSearch(query: string, config?: Config, signal?: A
|
|
|
348
358
|
logger.warn(`[BrowserManager] Leader handover during search (handover attempts left: ${handoverRetries}): ${briefErrorMessage(error)}...`);
|
|
349
359
|
await recoverFromLeaderHandover(container);
|
|
350
360
|
await new Promise((resolve) => setTimeout(resolve, HANDOVER_BACKOFF_MS + Math.floor(Math.random() * 400)));
|
|
351
|
-
return runWorkerSearch(query, config, signal, retries, sessionId, container, handoverRetries - 1);
|
|
361
|
+
return runWorkerSearch(query, config, signal, retries, sessionId, container, handoverRetries - 1, onTaskEvent);
|
|
352
362
|
}
|
|
353
363
|
|
|
354
364
|
if (retries > 0 && isTransientSocketError(error) && !isTaskTimeoutError(error) && !isCloudflareBlockError(error)) {
|
|
@@ -371,7 +381,7 @@ export async function runWorkerSearch(query: string, config?: Config, signal?: A
|
|
|
371
381
|
|
|
372
382
|
const jitter = 100 + Math.floor(Math.random() * 400);
|
|
373
383
|
await new Promise(resolve => setTimeout(resolve, jitter));
|
|
374
|
-
return runWorkerSearch(query, config, signal, retries - 1, sessionId, container, handoverRetries);
|
|
384
|
+
return runWorkerSearch(query, config, signal, retries - 1, sessionId, container, handoverRetries, onTaskEvent);
|
|
375
385
|
}
|
|
376
386
|
throw error;
|
|
377
387
|
}
|
|
@@ -134,29 +134,45 @@ export async function initBrowser(): Promise<void> {
|
|
|
134
134
|
// (consumers install without our lockfile, so a floating range resolves
|
|
135
135
|
// to a newer, broken version):
|
|
136
136
|
//
|
|
137
|
-
// • camoufox-js ^0.10.2 →
|
|
138
|
-
//
|
|
139
|
-
//
|
|
140
|
-
//
|
|
137
|
+
// • camoufox-js ^0.10.2 → the two ORIGINAL reasons for this pin have
|
|
138
|
+
// both lapsed (camoufox restored Windows assets in v152.0.4-beta.26,
|
|
139
|
+
// 2026-07-16; and impit's `only-allow pnpm` guard existed only in
|
|
140
|
+
// 0.13.1/0.14.0), but a THIRD, worse one replaced them. 0.12.0
|
|
141
|
+
// requires `better-sqlite3 ^13.0.1`, and better-sqlite3 13 DROPPED
|
|
142
|
+
// PREBUILT BINARIES: 12.x installs via `prebuild-install || node-gyp
|
|
143
|
+
// rebuild`, 13.x has no install script at all, so npm falls back to
|
|
144
|
+
// auto-running `node-gyp rebuild` for its binding.gyp and every
|
|
145
|
+
// install needs a full C++ toolchain. Measured: `npm ci` fails
|
|
146
|
+
// outright on the Windows CI runner ("could not find a version of
|
|
147
|
+
// Visual Studio 2017 or newer"), which is most Windows consumers.
|
|
148
|
+
// Upgrading again means checking better-sqlite3 first, not camoufox.
|
|
141
149
|
// • playwright-core 1.60.0 (exact) → playwright-core and the camoufox
|
|
142
150
|
// binary are one matched Juggler-protocol pair. 1.61 added a
|
|
143
|
-
// viewport.isMobile field the
|
|
151
|
+
// viewport.isMobile field the build REJECTS ("property
|
|
144
152
|
// viewport.isMobile not described in this scheme"), failing every
|
|
145
|
-
// launch below.
|
|
146
|
-
//
|
|
153
|
+
// launch below. Corroborated upstream: camoufox-js 0.12.0 declares
|
|
154
|
+
// `peerDependencies: playwright-core "<1.61.0"`, the same bound this
|
|
155
|
+
// pin has held by hand.
|
|
147
156
|
// • impit 0.13.0 (exact, a direct dep though only camoufox-js uses it)
|
|
148
|
-
// →
|
|
149
|
-
//
|
|
150
|
-
// `
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
// a published package
|
|
157
|
+
// → camoufox-js 0.10.2 requires `impit ^0.13.0`, and within that range
|
|
158
|
+
// 0.13.0 is the only version free of the `preinstall: npx only-allow
|
|
159
|
+
// pnpm` guard (an upstream mistake in 0.13.1/0.14.0, dropped again in
|
|
160
|
+
// 0.14.1) that hard-fails `npm install -g`. The exact pin is also the
|
|
161
|
+
// ONLY way to force a version for consumers, since npm `overrides`
|
|
162
|
+
// don't propagate to installers of a published package.
|
|
154
163
|
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
164
|
+
// What is NOT pinned: the browser BINARY. `camoufox-js fetch` takes no
|
|
165
|
+
// version argument — it walks the GitHub releases newest-first and takes
|
|
166
|
+
// the first non-prerelease one with an asset for this OS/arch. A consumer
|
|
167
|
+
// therefore gets whatever camoufox published most recently, whichever
|
|
168
|
+
// camoufox-js they have, and a future release could break launches with no
|
|
169
|
+
// change on our side. Both the current newest (v152.0.4-beta.28, Firefox
|
|
170
|
+
// 152) and the older v135.0.1-beta.24 an existing cache may hold were
|
|
171
|
+
// verified launching and driving under playwright-core 1.60.0.
|
|
172
|
+
//
|
|
173
|
+
// Always re-verify a REAL headless run after touching any of this: the
|
|
174
|
+
// unit and integration suites mock the browser, so a Juggler mismatch
|
|
175
|
+
// reaches production with a fully green suite.
|
|
160
176
|
let CamoufoxModule: any;
|
|
161
177
|
try {
|
|
162
178
|
CamoufoxModule = await import('camoufox-js');
|
|
@@ -228,11 +244,28 @@ export async function initBrowser(): Promise<void> {
|
|
|
228
244
|
// timeout races ahead.
|
|
229
245
|
launchPromise.catch((err: Error) => logToDebugFile('DEBUG', `[ThreadWorker] Background browser launch rejection: ${err.message}`));
|
|
230
246
|
let launchTimeoutId: NodeJS.Timeout | undefined;
|
|
247
|
+
let raceLost = false;
|
|
248
|
+
// If the launch resolves AFTER we gave up on it, nobody holds the result:
|
|
249
|
+
// `browser` is assigned only from the race's winner, so the catch below —
|
|
250
|
+
// whose comment promises to "close any partially-launched browser" — finds
|
|
251
|
+
// null and closes nothing. Camoufox's own launch bound is 180s against our
|
|
252
|
+
// 90s, so a launch completing in that window leaves a fully-started headless
|
|
253
|
+
// Firefox with no owner, surviving the orphan sweep (its parent worker is
|
|
254
|
+
// alive) until the pool shuts down. Adopt and close it, exactly as
|
|
255
|
+
// createPageSafe already does for an orphaned page.
|
|
256
|
+
launchPromise.then((b: any) => {
|
|
257
|
+
if (!raceLost) return;
|
|
258
|
+
logToDebugFile('WARN', `[Worker-${workerId}] Browser launch completed after its deadline; closing the orphaned instance.`);
|
|
259
|
+
try { b?.close?.(); } catch { /* already gone */ }
|
|
260
|
+
}).catch(() => { /* rejection already logged above */ });
|
|
231
261
|
try {
|
|
232
262
|
return await Promise.race([
|
|
233
263
|
launchPromise,
|
|
234
264
|
new Promise<never>((_, reject) => {
|
|
235
|
-
launchTimeoutId = setTimeout(() =>
|
|
265
|
+
launchTimeoutId = setTimeout(() => {
|
|
266
|
+
raceLost = true;
|
|
267
|
+
reject(new Error(`Browser launch timed out after ${launchTimeoutMs}ms`));
|
|
268
|
+
}, launchTimeoutMs);
|
|
236
269
|
})
|
|
237
270
|
]);
|
|
238
271
|
} finally {
|
|
@@ -267,10 +300,23 @@ export async function initBrowser(): Promise<void> {
|
|
|
267
300
|
const contextPromise = browser.newContext();
|
|
268
301
|
contextPromise.catch((err: Error) => logToDebugFile('DEBUG', `[ThreadWorker] Background browser context rejection: ${err.message}`));
|
|
269
302
|
let contextTimeoutId: NodeJS.Timeout | undefined;
|
|
303
|
+
let contextRaceLost = false;
|
|
304
|
+
// Same adoption as the launch above. Here the catch below CAN close the
|
|
305
|
+
// browser (it is assigned by now), which disposes the stray context with it —
|
|
306
|
+
// but only when initBrowser is entered with a dead browser, and it leaves the
|
|
307
|
+
// context open for the window in between. Closing it directly is unconditional.
|
|
308
|
+
contextPromise.then((c: any) => {
|
|
309
|
+
if (!contextRaceLost) return;
|
|
310
|
+
logToDebugFile('WARN', `[Worker-${workerId}] Browser context creation completed after its deadline; closing the orphaned context.`);
|
|
311
|
+
try { c?.close?.(); } catch { /* already gone */ }
|
|
312
|
+
}).catch(() => { /* rejection already logged above */ });
|
|
270
313
|
context = await Promise.race([
|
|
271
314
|
contextPromise,
|
|
272
315
|
new Promise<never>((_, reject) => {
|
|
273
|
-
contextTimeoutId = setTimeout(() =>
|
|
316
|
+
contextTimeoutId = setTimeout(() => {
|
|
317
|
+
contextRaceLost = true;
|
|
318
|
+
reject(new Error('Browser context creation timed out after 30000ms'));
|
|
319
|
+
}, 30000);
|
|
274
320
|
})
|
|
275
321
|
]);
|
|
276
322
|
if (contextTimeoutId) clearTimeout(contextTimeoutId);
|
|
@@ -259,15 +259,42 @@ async function extractSearchResults(page: any): Promise<any[]> {
|
|
|
259
259
|
/**
|
|
260
260
|
* Execute a search task
|
|
261
261
|
*/
|
|
262
|
+
/**
|
|
263
|
+
* Bind a task-timeout signal to the page it is bounding.
|
|
264
|
+
*
|
|
265
|
+
* `page.setDefaultTimeout` bounds Playwright's action and navigation verbs
|
|
266
|
+
* individually; it does not bound their sum, and it does not bound a Response read
|
|
267
|
+
* at all. Closing the page is what actually ends the work: any in-flight call
|
|
268
|
+
* rejects with 'Target closed', which runTask's catch already maps back to the
|
|
269
|
+
* task timeout. Fire-and-forget, `{ once }` so the listener cannot outlive the task.
|
|
270
|
+
*/
|
|
271
|
+
function bindTaskAbortToPage(page: any, signal?: AbortSignal): void {
|
|
272
|
+
if (!signal) return;
|
|
273
|
+
const onAbort = () => { page.close().catch(() => {}); };
|
|
274
|
+
if (signal.aborted) onAbort();
|
|
275
|
+
else signal.addEventListener('abort', onAbort, { once: true });
|
|
276
|
+
}
|
|
277
|
+
|
|
262
278
|
export async function executeSearchTask(
|
|
263
279
|
_context: any,
|
|
264
|
-
query: string
|
|
280
|
+
query: string,
|
|
281
|
+
signal?: AbortSignal
|
|
265
282
|
): Promise<{ results: any[]; jitter: number }> {
|
|
266
283
|
const page = await createPageSafe(_context);
|
|
267
284
|
const SEARCH_TIMEOUT = parseTimeoutMs(process.env['PI_RESEARCH_SEARCH_TIMEOUT_MS'], 45000);
|
|
268
285
|
page.setDefaultTimeout(SEARCH_TIMEOUT);
|
|
269
286
|
page.setDefaultNavigationTimeout(SEARCH_TIMEOUT);
|
|
270
287
|
|
|
288
|
+
// Same binding the scrape path has used all along, and for the same reason: the
|
|
289
|
+
// worker's task deadline was armed but its signal was only ever handed to
|
|
290
|
+
// executeScrapeTask, so on a search it fired into nothing. Playwright's own
|
|
291
|
+
// per-action timeouts still bound each verb, but the sum of them plus the jitter
|
|
292
|
+
// sleep can outlast the whole task budget — and poolifier keeps counting this node
|
|
293
|
+
// as executing until the worker replies, so the node stays pinned long after the
|
|
294
|
+
// caller was answered. Closing the page rejects whatever is in flight with
|
|
295
|
+
// 'Target closed', which runTask's catch already recognises as the timeout.
|
|
296
|
+
bindTaskAbortToPage(page, signal);
|
|
297
|
+
|
|
271
298
|
try {
|
|
272
299
|
logToDebugFile('DEBUG', `[Worker-${workerId}] Starting search for: ${query}`);
|
|
273
300
|
|
|
@@ -692,11 +719,15 @@ const HEALTH_FALLBACK_URL = 'https://example.com/';
|
|
|
692
719
|
async function executeHealthCheckAttempt(
|
|
693
720
|
_context: any,
|
|
694
721
|
navTimeoutMs: number,
|
|
695
|
-
url: string = HEALTH_PRIMARY_URL
|
|
722
|
+
url: string = HEALTH_PRIMARY_URL,
|
|
723
|
+
signal?: AbortSignal
|
|
696
724
|
): Promise<{ success: boolean; navMs: number }> {
|
|
697
725
|
const page = await createPageSafe(_context);
|
|
698
726
|
page.setDefaultTimeout(navTimeoutMs);
|
|
699
727
|
page.setDefaultNavigationTimeout(navTimeoutMs);
|
|
728
|
+
// See executeSearchTask: the probe runs twice (primary then fallback), so its own
|
|
729
|
+
// nav timeout bounds each attempt but not the pair.
|
|
730
|
+
bindTaskAbortToPage(page, signal);
|
|
700
731
|
|
|
701
732
|
try {
|
|
702
733
|
const navStart = Date.now();
|
|
@@ -721,7 +752,8 @@ async function executeHealthCheckAttempt(
|
|
|
721
752
|
*/
|
|
722
753
|
export async function executeHealthCheck(
|
|
723
754
|
_context: any,
|
|
724
|
-
initMs: number
|
|
755
|
+
initMs: number,
|
|
756
|
+
signal?: AbortSignal
|
|
725
757
|
): Promise<{ success: boolean; navMs: number }> {
|
|
726
758
|
// Nav timeout: read from env (passed through getBrowserEnv), floor at 10s.
|
|
727
759
|
// The outer BrowserTaskScheduler.runHealthCheck() has its own 45s hard deadline.
|
|
@@ -733,14 +765,14 @@ export async function executeHealthCheck(
|
|
|
733
765
|
}
|
|
734
766
|
|
|
735
767
|
try {
|
|
736
|
-
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT);
|
|
768
|
+
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_PRIMARY_URL, signal);
|
|
737
769
|
} catch (firstError: unknown) {
|
|
738
770
|
// One retry: if the first attempt fails, the page may have been in a bad state
|
|
739
771
|
// (challenge redirect, partial load, transient network blip). A second attempt
|
|
740
772
|
// on a fresh page helps distinguish a real outage from a one-off failure.
|
|
741
773
|
logToDebugFile('WARN', `[Worker-${workerId}] Health check attempt 1 failed: ${firstError instanceof Error ? firstError.message : String(firstError)}. Retrying once...`);
|
|
742
774
|
try {
|
|
743
|
-
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT);
|
|
775
|
+
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_PRIMARY_URL, signal);
|
|
744
776
|
} catch (retryError: unknown) {
|
|
745
777
|
const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
746
778
|
// A TIMEOUT is left to propagate: upstream (isBusyPoolHealthFailure) already
|
|
@@ -756,7 +788,7 @@ export async function executeHealthCheck(
|
|
|
756
788
|
// healthy enough to research (search has its own retries/fallbacks downstream).
|
|
757
789
|
logToDebugFile('WARN', `[Worker-${workerId}] Search-provider health probe failed twice (${retryMsg}); verifying open-web reachability via fallback endpoint...`);
|
|
758
790
|
try {
|
|
759
|
-
const fallback = await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_FALLBACK_URL);
|
|
791
|
+
const fallback = await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_FALLBACK_URL, signal);
|
|
760
792
|
logToDebugFile('WARN', `[Worker-${workerId}] Fallback endpoint reachable — open web is up, search provider is degraded. Proceeding with research.`);
|
|
761
793
|
return fallback;
|
|
762
794
|
} catch (fallbackError: unknown) {
|
|
@@ -99,7 +99,10 @@ var init_log_utils = __esm({
|
|
|
99
99
|
`${B_LEFT}(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_RIGHT}`,
|
|
100
100
|
"g"
|
|
101
101
|
);
|
|
102
|
-
JWT_PATTERN = new RegExp(
|
|
102
|
+
JWT_PATTERN = new RegExp(
|
|
103
|
+
`${B_LEFT}eyJ[A-Za-z0-9_-]{1,1024}\\.[A-Za-z0-9_-]{1,8192}\\.[A-Za-z0-9_-]{1,4096}`,
|
|
104
|
+
"g"
|
|
105
|
+
);
|
|
103
106
|
BASIC_AUTH_PATTERN = new RegExp(`${B_LEFT}Basic\\s+[A-Za-z0-9+/]{16,}={0,2}`, "gi");
|
|
104
107
|
BEARER_AUTH_PATTERN = new RegExp(`${B_LEFT}Bearer\\s+[A-Za-z0-9._~+/-]+=*`, "gi");
|
|
105
108
|
PROVIDER_TOKEN_PATTERN = new RegExp(
|
|
@@ -1057,6 +1060,17 @@ var init_logger = __esm({
|
|
|
1057
1060
|
timestamp,
|
|
1058
1061
|
level,
|
|
1059
1062
|
...getLogContext(),
|
|
1063
|
+
// Every process writes to the SAME log file — up to three concurrent research
|
|
1064
|
+
// runs plus the elected browser-pool leader — and until this field existed
|
|
1065
|
+
// there was nothing to tell their lines apart. Reconstructing a lock
|
|
1066
|
+
// contention from the log then meant inferring process boundaries from
|
|
1067
|
+
// timing, which is guesswork: two runs a second apart interleave into what
|
|
1068
|
+
// reads like one process contradicting itself. Twelve bytes a line buys
|
|
1069
|
+
// attributable forensics.
|
|
1070
|
+
//
|
|
1071
|
+
// After the context spread, not before: which process wrote a line is not
|
|
1072
|
+
// something a scoped context gets to claim otherwise.
|
|
1073
|
+
pid: process.pid,
|
|
1060
1074
|
message,
|
|
1061
1075
|
...firstError ? { errorMessage: redactSecrets(firstError.message), errorStack: redactSecrets(firstError.stack ?? "") } : {}
|
|
1062
1076
|
};
|
|
@@ -1163,7 +1177,7 @@ var init_metrics = __esm({
|
|
|
1163
1177
|
increment(name, value = 1, labels) {
|
|
1164
1178
|
const key = this.getKey(name, labels);
|
|
1165
1179
|
this.counters.set(key, (this.counters.get(key) ?? 0) + value);
|
|
1166
|
-
if (name
|
|
1180
|
+
if (name.startsWith("llm_")) {
|
|
1167
1181
|
logger.debug(`[Metrics] Incremented ${name} by ${value}${labels ? ` (labels: ${JSON.stringify(labels)})` : ""}`);
|
|
1168
1182
|
}
|
|
1169
1183
|
}
|
|
@@ -1267,6 +1281,28 @@ var init_metrics = __esm({
|
|
|
1267
1281
|
async measure(name, action, labels) {
|
|
1268
1282
|
return this.getActive().measure(name, action, labels);
|
|
1269
1283
|
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Emit API that ALWAYS targets the session registry, ignoring any active run
|
|
1286
|
+
* context.
|
|
1287
|
+
*
|
|
1288
|
+
* For process-lifetime callbacks that happen to be *registered* during a run.
|
|
1289
|
+
* AsyncLocalStorage resolves at emit time from the async resource that fired,
|
|
1290
|
+
* and for a listener installed on a long-lived emitter that resource is the
|
|
1291
|
+
* emitter itself — created, for every lazily-initialized subsystem here, inside
|
|
1292
|
+
* whichever run first needed it. So a worker-pool error handler installed during
|
|
1293
|
+
* run 1 keeps writing into run 1's registry for the rest of the process: those
|
|
1294
|
+
* counts disappear from every later run's summary (the registry was discarded
|
|
1295
|
+
* when the run ended) and the dead registry stays reachable through the closure.
|
|
1296
|
+
*
|
|
1297
|
+
* Use this only where the emission is genuinely process-scoped. Anything that
|
|
1298
|
+
* describes work a run asked for belongs in the run registry, which is what the
|
|
1299
|
+
* plain emit methods above give you.
|
|
1300
|
+
*/
|
|
1301
|
+
session = {
|
|
1302
|
+
increment: (name, value = 1, labels) => this._session.increment(name, value, labels),
|
|
1303
|
+
setGauge: (name, value, labels) => this._session.setGauge(name, value, labels),
|
|
1304
|
+
observe: (name, value, labels) => this._session.observe(name, value, labels)
|
|
1305
|
+
};
|
|
1270
1306
|
// ── Session-level read API ─────────────────────────────────────────────
|
|
1271
1307
|
/** Snapshot of the session registry (infrastructure / cross-run metrics). */
|
|
1272
1308
|
getSessionSnapshot() {
|
|
@@ -1509,6 +1545,7 @@ var scraper_utils_exports = {};
|
|
|
1509
1545
|
__export(scraper_utils_exports, {
|
|
1510
1546
|
createJsMarkdownConverter: () => createJsMarkdownConverter,
|
|
1511
1547
|
createNativeMarkdownConverter: () => createNativeMarkdownConverter,
|
|
1548
|
+
disposeSsrfSafeFetcher: () => disposeSsrfSafeFetcher,
|
|
1512
1549
|
extractDomain: () => extractDomain,
|
|
1513
1550
|
formatErrorWithCause: () => formatErrorWithCause,
|
|
1514
1551
|
getRandomUserAgent: () => getRandomUserAgent,
|
|
@@ -1644,19 +1681,33 @@ function isSsrfBlockError(error) {
|
|
|
1644
1681
|
}
|
|
1645
1682
|
return false;
|
|
1646
1683
|
}
|
|
1647
|
-
|
|
1648
|
-
|
|
1684
|
+
function getSsrfSafeFetcher() {
|
|
1685
|
+
cachedSsrfFetcher ??= (async () => {
|
|
1686
|
+
try {
|
|
1687
|
+
const { Agent, fetch: undiciFetch } = await import("undici");
|
|
1688
|
+
return {
|
|
1689
|
+
fetch: undiciFetch,
|
|
1690
|
+
dispatcher: new Agent({ connect: { lookup: ssrfSafeLookup } })
|
|
1691
|
+
};
|
|
1692
|
+
} catch (e) {
|
|
1693
|
+
logger.warn(`[Scrapers] undici unavailable; connect-time SSRF pinning disabled (request-time validation still active): ${e instanceof Error ? e.message : String(e)}`);
|
|
1694
|
+
return null;
|
|
1695
|
+
}
|
|
1696
|
+
})();
|
|
1697
|
+
return cachedSsrfFetcher;
|
|
1698
|
+
}
|
|
1699
|
+
async function disposeSsrfSafeFetcher() {
|
|
1700
|
+
const pending = cachedSsrfFetcher;
|
|
1701
|
+
cachedSsrfFetcher = void 0;
|
|
1702
|
+
if (pending === void 0) return;
|
|
1703
|
+
const cached = await pending.catch(() => null);
|
|
1704
|
+
const closable = cached?.dispatcher;
|
|
1705
|
+
if (!closable?.close) return;
|
|
1649
1706
|
try {
|
|
1650
|
-
|
|
1651
|
-
cachedSsrfFetcher = {
|
|
1652
|
-
fetch: undiciFetch,
|
|
1653
|
-
dispatcher: new Agent({ connect: { lookup: ssrfSafeLookup } })
|
|
1654
|
-
};
|
|
1707
|
+
await closable.close();
|
|
1655
1708
|
} catch (e) {
|
|
1656
|
-
logger.
|
|
1657
|
-
cachedSsrfFetcher = null;
|
|
1709
|
+
logger.debug(`[Scrapers] Error closing SSRF dispatcher: ${e instanceof Error ? e.message : String(e)}`);
|
|
1658
1710
|
}
|
|
1659
|
-
return cachedSsrfFetcher;
|
|
1660
1711
|
}
|
|
1661
1712
|
function formatErrorWithCause(err) {
|
|
1662
1713
|
try {
|
|
@@ -2152,11 +2203,21 @@ async function extractSearchResults(page) {
|
|
|
2152
2203
|
return found;
|
|
2153
2204
|
});
|
|
2154
2205
|
}
|
|
2155
|
-
|
|
2206
|
+
function bindTaskAbortToPage(page, signal) {
|
|
2207
|
+
if (!signal) return;
|
|
2208
|
+
const onAbort = () => {
|
|
2209
|
+
page.close().catch(() => {
|
|
2210
|
+
});
|
|
2211
|
+
};
|
|
2212
|
+
if (signal.aborted) onAbort();
|
|
2213
|
+
else signal.addEventListener("abort", onAbort, { once: true });
|
|
2214
|
+
}
|
|
2215
|
+
async function executeSearchTask(_context, query, signal) {
|
|
2156
2216
|
const page = await createPageSafe(_context);
|
|
2157
2217
|
const SEARCH_TIMEOUT = parseTimeoutMs(process.env["PI_RESEARCH_SEARCH_TIMEOUT_MS"], 45e3);
|
|
2158
2218
|
page.setDefaultTimeout(SEARCH_TIMEOUT);
|
|
2159
2219
|
page.setDefaultNavigationTimeout(SEARCH_TIMEOUT);
|
|
2220
|
+
bindTaskAbortToPage(page, signal);
|
|
2160
2221
|
try {
|
|
2161
2222
|
logToDebugFile2("DEBUG", `[Worker-${workerId2}] Starting search for: ${query}`);
|
|
2162
2223
|
if (process.env["PI_RESEARCH_MOCK_SEARCH"] === "true") {
|
|
@@ -2369,10 +2430,11 @@ async function executeScrapeTask(_context, url, signal) {
|
|
|
2369
2430
|
}
|
|
2370
2431
|
var HEALTH_PRIMARY_URL = "https://lite.duckduckgo.com/lite/";
|
|
2371
2432
|
var HEALTH_FALLBACK_URL = "https://example.com/";
|
|
2372
|
-
async function executeHealthCheckAttempt(_context, navTimeoutMs, url = HEALTH_PRIMARY_URL) {
|
|
2433
|
+
async function executeHealthCheckAttempt(_context, navTimeoutMs, url = HEALTH_PRIMARY_URL, signal) {
|
|
2373
2434
|
const page = await createPageSafe(_context);
|
|
2374
2435
|
page.setDefaultTimeout(navTimeoutMs);
|
|
2375
2436
|
page.setDefaultNavigationTimeout(navTimeoutMs);
|
|
2437
|
+
bindTaskAbortToPage(page, signal);
|
|
2376
2438
|
try {
|
|
2377
2439
|
const navStart = Date.now();
|
|
2378
2440
|
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
@@ -2388,25 +2450,25 @@ async function executeHealthCheckAttempt(_context, navTimeoutMs, url = HEALTH_PR
|
|
|
2388
2450
|
throw error;
|
|
2389
2451
|
}
|
|
2390
2452
|
}
|
|
2391
|
-
async function executeHealthCheck(_context, initMs) {
|
|
2453
|
+
async function executeHealthCheck(_context, initMs, signal) {
|
|
2392
2454
|
const configuredMs = parseTimeoutMs(process.env["PI_RESEARCH_HEALTH_CHECK_TIMEOUT_MS"], 0);
|
|
2393
2455
|
const HEALTH_TIMEOUT = configuredMs > 0 ? Math.max(1e4, configuredMs) : 1e4;
|
|
2394
2456
|
if (initMs > 3e3) {
|
|
2395
2457
|
logToDebugFile2("WARN", `[Worker-${workerId2}] Browser init was slow: ${initMs}ms \u2014 leaving less headroom for DDG navigation.`);
|
|
2396
2458
|
}
|
|
2397
2459
|
try {
|
|
2398
|
-
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT);
|
|
2460
|
+
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_PRIMARY_URL, signal);
|
|
2399
2461
|
} catch (firstError) {
|
|
2400
2462
|
logToDebugFile2("WARN", `[Worker-${workerId2}] Health check attempt 1 failed: ${firstError instanceof Error ? firstError.message : String(firstError)}. Retrying once...`);
|
|
2401
2463
|
try {
|
|
2402
|
-
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT);
|
|
2464
|
+
return await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_PRIMARY_URL, signal);
|
|
2403
2465
|
} catch (retryError) {
|
|
2404
2466
|
const retryMsg = retryError instanceof Error ? retryError.message : String(retryError);
|
|
2405
2467
|
const isTimeout = /tim(e|ed)\s*out|timeout/i.test(retryMsg);
|
|
2406
2468
|
if (!isTimeout) {
|
|
2407
2469
|
logToDebugFile2("WARN", `[Worker-${workerId2}] Search-provider health probe failed twice (${retryMsg}); verifying open-web reachability via fallback endpoint...`);
|
|
2408
2470
|
try {
|
|
2409
|
-
const fallback = await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_FALLBACK_URL);
|
|
2471
|
+
const fallback = await executeHealthCheckAttempt(_context, HEALTH_TIMEOUT, HEALTH_FALLBACK_URL, signal);
|
|
2410
2472
|
logToDebugFile2("WARN", `[Worker-${workerId2}] Fallback endpoint reachable \u2014 open web is up, search provider is degraded. Proceeding with research.`);
|
|
2411
2473
|
return fallback;
|
|
2412
2474
|
} catch (fallbackError) {
|
|
@@ -2510,7 +2572,7 @@ var ConfigSchema = Type.Object({
|
|
|
2510
2572
|
* never killed before it has used its full nav budget plus time waiting in the queue.
|
|
2511
2573
|
* (default: 10000) */
|
|
2512
2574
|
BROWSER_TASK_TIMEOUT_MS: Type.Number({ minimum: 2e3, maximum: 12e4, default: 1e4 }),
|
|
2513
|
-
/** Timeout for coordinator/
|
|
2575
|
+
/** Timeout for coordinator/research-lead/repair/knowledge LLM calls in ms (default: 300000 = 5 min, range: 60s-30min).
|
|
2514
2576
|
* Not exposed in TUI — controlled via PI_RESEARCH_LLM_TIMEOUT_MS env var.
|
|
2515
2577
|
* Ceiling raised to 30min (matching RESEARCHER_TIMEOUT_MS): a slow model on a
|
|
2516
2578
|
* large-context synthesis/plan call can legitimately run >10min, and the old
|
|
@@ -2518,7 +2580,7 @@ var ConfigSchema = Type.Object({
|
|
|
2518
2580
|
* (e.g. 900000), causing those calls to time out mid-generation. */
|
|
2519
2581
|
LLM_TIMEOUT_MS: Type.Number({ minimum: 6e4, maximum: 18e5, default: 3e5 }),
|
|
2520
2582
|
/** Chain-of-thought "thinking" level for the engine's own LLM calls (coordinator,
|
|
2521
|
-
*
|
|
2583
|
+
* router, synthesizer, JSON-repair, knowledge extraction) AND the researcher
|
|
2522
2584
|
* sub-agents. Default 'off': these calls emit structured JSON / cited reports, not
|
|
2523
2585
|
* open-ended reasoning, so thinking only burns the output-token budget (often
|
|
2524
2586
|
* truncating before the JSON/report block) and adds latency for at most a marginal
|
|
@@ -2533,22 +2595,26 @@ var ConfigSchema = Type.Object({
|
|
|
2533
2595
|
Type.Literal("medium"),
|
|
2534
2596
|
Type.Literal("high")
|
|
2535
2597
|
], { default: "off" }),
|
|
2536
|
-
/** Max output tokens for the coordinator plan
|
|
2537
|
-
*
|
|
2598
|
+
/** Max output tokens for the coordinator's round-1 plan. Default 16384. The routing
|
|
2599
|
+
* decision has its OWN, much smaller ceiling (it emits a decision, never a report,
|
|
2600
|
+
* and cannot synthesize mid-round under the split protocol); the final report uses
|
|
2601
|
+
* SYNTHESIS_MAX_TOKENS below. The blanket
|
|
2538
2602
|
* 4096 cap previously throttled every call and could be exhausted by a thinking block
|
|
2539
2603
|
* before any text was emitted. Capped at the model's real ceiling at call time.
|
|
2540
2604
|
* Not exposed in TUI — controlled via PI_RESEARCH_PLANNING_MAX_TOKENS env var. */
|
|
2541
2605
|
PLANNING_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 16384 }),
|
|
2542
|
-
/** Max output tokens for the final synthesized research report (the
|
|
2543
|
-
*
|
|
2606
|
+
/** Max output tokens for the final synthesized research report (the synthesizer call,
|
|
2607
|
+
* and each pass of a reduce when the corpus does not fit in one). Default 32768 so a
|
|
2608
|
+
* full cited report is never truncated. It is also the reserve subtracted from the
|
|
2609
|
+
* model's context window when budgeting that corpus. Capped at
|
|
2544
2610
|
* the model's real maxTokens at call time.
|
|
2545
2611
|
* Not exposed in TUI — controlled via PI_RESEARCH_SYNTHESIS_MAX_TOKENS env var. */
|
|
2546
2612
|
SYNTHESIS_MAX_TOKENS: Type.Number({ minimum: 1024, maximum: 131072, default: 32768 }),
|
|
2547
2613
|
/** LLM Model override for researcher sub-agents and knowledge synthesis.
|
|
2548
2614
|
* Format: provider/model-id (e.g. google/gemini-2.0-flash-001) or just model-id.
|
|
2549
2615
|
* When set, this overrides ctx.model for researcher sub-agents (both deep and quick)
|
|
2550
|
-
* and the knowledge synthesis background LLM. The coordinator and
|
|
2551
|
-
* use the caller's model (ctx.model).
|
|
2616
|
+
* and the knowledge synthesis background LLM. The coordinator and the research lead
|
|
2617
|
+
* always use the caller's model (ctx.model).
|
|
2552
2618
|
*/
|
|
2553
2619
|
RESEARCH_MODEL: Type.Optional(Type.String()),
|
|
2554
2620
|
/** Explicit directory for the knowledge store database (overrides default) */
|
|
@@ -2602,6 +2668,7 @@ function resolveHeadlessMode() {
|
|
|
2602
2668
|
if (process.env["PI_RESEARCH_USE_XVFB"] === "true") return "virtual";
|
|
2603
2669
|
return true;
|
|
2604
2670
|
}
|
|
2671
|
+
var HEALTHCHECK_MIN_BUDGET_MS = 45e3 + 6e4;
|
|
2605
2672
|
|
|
2606
2673
|
// src/infrastructure/browser/thread-worker-browser.ts
|
|
2607
2674
|
var browser = null;
|
|
@@ -2716,11 +2783,24 @@ async function initBrowser() {
|
|
|
2716
2783
|
});
|
|
2717
2784
|
launchPromise.catch((err) => logToDebugFile3("DEBUG", `[ThreadWorker] Background browser launch rejection: ${err.message}`));
|
|
2718
2785
|
let launchTimeoutId;
|
|
2786
|
+
let raceLost = false;
|
|
2787
|
+
launchPromise.then((b) => {
|
|
2788
|
+
if (!raceLost) return;
|
|
2789
|
+
logToDebugFile3("WARN", `[Worker-${workerId3}] Browser launch completed after its deadline; closing the orphaned instance.`);
|
|
2790
|
+
try {
|
|
2791
|
+
b?.close?.();
|
|
2792
|
+
} catch {
|
|
2793
|
+
}
|
|
2794
|
+
}).catch(() => {
|
|
2795
|
+
});
|
|
2719
2796
|
try {
|
|
2720
2797
|
return await Promise.race([
|
|
2721
2798
|
launchPromise,
|
|
2722
2799
|
new Promise((_, reject) => {
|
|
2723
|
-
launchTimeoutId = setTimeout(() =>
|
|
2800
|
+
launchTimeoutId = setTimeout(() => {
|
|
2801
|
+
raceLost = true;
|
|
2802
|
+
reject(new Error(`Browser launch timed out after ${launchTimeoutMs}ms`));
|
|
2803
|
+
}, launchTimeoutMs);
|
|
2724
2804
|
})
|
|
2725
2805
|
]);
|
|
2726
2806
|
} finally {
|
|
@@ -2745,10 +2825,23 @@ async function initBrowser() {
|
|
|
2745
2825
|
const contextPromise = browser.newContext();
|
|
2746
2826
|
contextPromise.catch((err) => logToDebugFile3("DEBUG", `[ThreadWorker] Background browser context rejection: ${err.message}`));
|
|
2747
2827
|
let contextTimeoutId;
|
|
2828
|
+
let contextRaceLost = false;
|
|
2829
|
+
contextPromise.then((c) => {
|
|
2830
|
+
if (!contextRaceLost) return;
|
|
2831
|
+
logToDebugFile3("WARN", `[Worker-${workerId3}] Browser context creation completed after its deadline; closing the orphaned context.`);
|
|
2832
|
+
try {
|
|
2833
|
+
c?.close?.();
|
|
2834
|
+
} catch {
|
|
2835
|
+
}
|
|
2836
|
+
}).catch(() => {
|
|
2837
|
+
});
|
|
2748
2838
|
context = await Promise.race([
|
|
2749
2839
|
contextPromise,
|
|
2750
2840
|
new Promise((_, reject) => {
|
|
2751
|
-
contextTimeoutId = setTimeout(() =>
|
|
2841
|
+
contextTimeoutId = setTimeout(() => {
|
|
2842
|
+
contextRaceLost = true;
|
|
2843
|
+
reject(new Error("Browser context creation timed out after 30000ms"));
|
|
2844
|
+
}, 3e4);
|
|
2752
2845
|
})
|
|
2753
2846
|
]);
|
|
2754
2847
|
if (contextTimeoutId) clearTimeout(contextTimeoutId);
|
|
@@ -2864,14 +2957,14 @@ async function runTask(data) {
|
|
|
2864
2957
|
let result;
|
|
2865
2958
|
if (type === "search") {
|
|
2866
2959
|
if (!query) throw new Error("Search task requires a query");
|
|
2867
|
-
const searchResult = await executeSearchTask(getContext(), query);
|
|
2960
|
+
const searchResult = await executeSearchTask(getContext(), query, abortController.signal);
|
|
2868
2961
|
result = { results: searchResult.results, duration: Date.now() - startTime, jitter: searchResult.jitter };
|
|
2869
2962
|
} else if (type === "scrape") {
|
|
2870
2963
|
if (!url) throw new Error("Scrape task requires a URL");
|
|
2871
2964
|
const scrapeResult = await executeScrapeTask(getContext(), url, abortController.signal);
|
|
2872
2965
|
result = { ...scrapeResult, duration: Date.now() - startTime };
|
|
2873
2966
|
} else if (type === "healthcheck") {
|
|
2874
|
-
const healthResult = await executeHealthCheck(getContext(), initMs);
|
|
2967
|
+
const healthResult = await executeHealthCheck(getContext(), initMs, abortController.signal);
|
|
2875
2968
|
result = { ...healthResult, duration: Date.now() - startTime };
|
|
2876
2969
|
} else {
|
|
2877
2970
|
result = { error: "Unknown task type", duration: Date.now() - startTime };
|
|
@@ -116,14 +116,14 @@ async function runTask(data: TaskData | undefined): Promise<TaskResult> {
|
|
|
116
116
|
let result: any;
|
|
117
117
|
if (type === 'search') {
|
|
118
118
|
if (!query) throw new Error('Search task requires a query');
|
|
119
|
-
const searchResult = await executeSearchTask(getContext(), query);
|
|
119
|
+
const searchResult = await executeSearchTask(getContext(), query, abortController.signal);
|
|
120
120
|
result = { results: searchResult.results, duration: Date.now() - startTime, jitter: searchResult.jitter };
|
|
121
121
|
} else if (type === 'scrape') {
|
|
122
122
|
if (!url) throw new Error('Scrape task requires a URL');
|
|
123
123
|
const scrapeResult = await executeScrapeTask(getContext(), url, abortController.signal);
|
|
124
124
|
result = { ...scrapeResult, duration: Date.now() - startTime };
|
|
125
125
|
} else if (type === 'healthcheck') {
|
|
126
|
-
const healthResult = await executeHealthCheck(getContext(), initMs);
|
|
126
|
+
const healthResult = await executeHealthCheck(getContext(), initMs, abortController.signal);
|
|
127
127
|
result = { ...healthResult, duration: Date.now() - startTime };
|
|
128
128
|
} else {
|
|
129
129
|
result = { error: 'Unknown task type', duration: Date.now() - startTime };
|