@lincoln504/pi-research 1.0.12 → 1.1.2
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 +20 -8
- package/dist/cli.mjs +210 -67
- package/dist/thread-worker.mjs +11 -1
- package/docs/ARCHITECTURE.md +24 -0
- package/docs/CHANGELOG.md +40 -0
- package/docs/CONFIGURATION.md +3 -1
- package/docs/KNOWLEDGE-STORE.md +1 -1
- package/docs/SDK.md +17 -5
- package/package.json +1 -1
- package/scripts/build.cjs +23 -5
- package/skills/pi-research/SKILL.md +13 -4
- package/skills/pi-research/references/configuration.md +1 -1
- package/src/cli.ts +54 -4
- package/src/core/interfaces/observer-interfaces.ts +9 -0
- package/src/core/llm/model-registry-factory.ts +40 -3
- package/src/core/llm/research-model-resolver.ts +2 -2
- package/src/infrastructure/browser/browser-error-utils.ts +19 -4
- package/src/infrastructure/browser/task-execution-service.ts +86 -9
- package/src/infrastructure/browser/thread-worker.mjs +11 -1
- package/src/infrastructure/research-run-semaphore.ts +43 -10
- package/src/orchestration/headless-observer.ts +4 -0
- package/src/orchestration/research-orchestration-service.ts +42 -7
- package/src/orchestration/research-synthesis-service.ts +1 -81
- package/src/tools/research-knowledge-search.ts +8 -2
- package/src/utils/circuit-breaker.ts +20 -0
- package/src/utils/citation-utils.ts +103 -7
- package/src/utils/llm-usage.ts +37 -0
- package/src/utils/metrics-summary.ts +25 -2
- package/src/utils/research-export.ts +9 -2
- package/src/web-research/retry-utils.ts +13 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
<strong>
|
|
6
|
+
<strong>Local web research and knowledge store</strong>
|
|
7
7
|
</p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
@@ -24,9 +24,20 @@
|
|
|
24
24
|
|
|
25
25
|
<a href="https://github.com/Lincoln504/pi-research/actions/workflows/ci.yml"><img alt="Build status" src="https://img.shields.io/github/actions/workflow/status/Lincoln504/pi-research/ci.yml?style=flat-square&branch=main" /></a> <a href="https://www.npmjs.com/package/@lincoln504/pi-research"><img alt="npm version" src="https://img.shields.io/npm/v/@lincoln504/pi-research.svg?style=flat-square" /></a>
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
**Free, unlimited web search & scraping — no monthly quota, no subscription.**
|
|
28
|
+
You pay only for the LLM tokens you use, and it builds your own private, searchable
|
|
29
|
+
knowledge store on your machine.
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
### How it works
|
|
32
|
+
|
|
33
|
+
A research run is a loop of agent teams: a coordinator plans the work and runs an
|
|
34
|
+
opening search, parallel researcher agents scrape and read sources through a fixed,
|
|
35
|
+
sandboxed toolkit (no shell access, and no ability to write or modify files), and an evaluator decides whether
|
|
36
|
+
to run another round or synthesize — returning one cited Markdown report while its
|
|
37
|
+
sources are saved to the knowledge store in the background.
|
|
38
|
+
|
|
39
|
+
Three depth levels — normal, deep, ultra — set the team size and number of rounds;
|
|
40
|
+
ask in natural language and the tool picks the right one:
|
|
30
41
|
|
|
31
42
|

|
|
32
43
|
|
|
@@ -39,12 +50,13 @@ Ask in natural language — the tool understands the depth needed:
|
|
|
39
50
|
- Using the pi-research agent skill as OpenClaw's web access.
|
|
40
51
|
- Building agent systems that identify and examine web sources.
|
|
41
52
|
|
|
42
|
-
###
|
|
53
|
+
### Why pi-research
|
|
43
54
|
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
55
|
+
- **Free, unlimited search & scrape — no quota, no monthly fee.** Most AI-search providers cap free searches per month and then charge; results run on *their* index on *their* servers. pi-research's search and scraping layer (DuckDuckGo via a stealth browser) is free and unlimited — you pay only for the LLM tokens you use, never per search or per month.
|
|
56
|
+
- **You own your index.** Every finding is written into a local [LanceDB](https://lancedb.com) knowledge store on *your* machine — a persistent, searchable index you control and keep, which also seeds future runs so repeat questions get faster, cheaper answers. No borrowed, server-side index you can't keep or query. Findings can also be kept **globally**, scoped to a **single project**, or **not kept at all** — set per directory, changed whenever you like. The optionality is the point: the store is yours to build, partition, or switch off.
|
|
57
|
+
- **Cited reports, not raw dumps.** It returns one synthesized, sourced Markdown report to the chat instead of dumping raw web content into the conversation — context-efficient and easy to verify.
|
|
58
|
+
- **Sandboxed by design.** Web access runs inside a specialized, limited research agent that cannot run shell commands or write, edit, or delete anything. It can read and search files in the working directory, so scope that directory as you would for any coding agent.
|
|
59
|
+
- **Search a little or a lot** — three depth levels in the pi tool (four via the SDK and standalone CLI, which add the quick depth-0 path). Levels 1 and 2 are recommended for everyday workflow use; level 3 is for larger-scale investigations.
|
|
48
60
|
|
|
49
61
|
### Requirements / limitations
|
|
50
62
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1709,9 +1709,23 @@ var init_metrics = __esm({
|
|
|
1709
1709
|
});
|
|
1710
1710
|
|
|
1711
1711
|
// src/utils/llm-usage.ts
|
|
1712
|
+
function warnIfUnpriced(model, tokens, cost) {
|
|
1713
|
+
if (tokens <= 0 || cost > 0) return;
|
|
1714
|
+
const m = model;
|
|
1715
|
+
const key = `${m.provider ?? "unknown"}/${m.id ?? "unknown"}`;
|
|
1716
|
+
if (unpricedModelsWarned.has(key)) return;
|
|
1717
|
+
const table = m.cost;
|
|
1718
|
+
const allZero = !table || Object.values(table).every((v) => typeof v !== "number" || v === 0);
|
|
1719
|
+
if (!allZero) return;
|
|
1720
|
+
unpricedModelsWarned.add(key);
|
|
1721
|
+
logger.warn(
|
|
1722
|
+
`[LlmUsage] Model "${key}" consumed ${tokens} tokens but reports $0.00 \u2014 its price table is all zeros. Expected for flat-rate plans and local servers. Otherwise the price data is missing: a hand-written "models" entry in ~/.pi/agent/models.json replaces pi's catalog entry (and its pricing) for the same model id \u2014 remove it and let pi's catalog supply the model, or use "modelOverrides", which merges.`
|
|
1723
|
+
);
|
|
1724
|
+
}
|
|
1712
1725
|
function recordLlmUsage(model, rawUsage, opts) {
|
|
1713
1726
|
if (!rawUsage) return { tokens: 0, cost: 0 };
|
|
1714
1727
|
const { tokens, cost } = extractUsage(model, rawUsage);
|
|
1728
|
+
warnIfUnpriced(model, tokens, cost);
|
|
1715
1729
|
if (tokens > 0 || cost > 0) {
|
|
1716
1730
|
const labels = { component: opts.component };
|
|
1717
1731
|
if (opts.complexity !== void 0) labels["complexity"] = String(opts.complexity);
|
|
@@ -1721,11 +1735,14 @@ function recordLlmUsage(model, rawUsage, opts) {
|
|
|
1721
1735
|
}
|
|
1722
1736
|
return { tokens, cost };
|
|
1723
1737
|
}
|
|
1738
|
+
var unpricedModelsWarned;
|
|
1724
1739
|
var init_llm_usage = __esm({
|
|
1725
1740
|
"src/utils/llm-usage.ts"() {
|
|
1726
1741
|
"use strict";
|
|
1727
1742
|
init_llm();
|
|
1743
|
+
init_logger();
|
|
1728
1744
|
init_metrics();
|
|
1745
|
+
unpricedModelsWarned = /* @__PURE__ */ new Set();
|
|
1729
1746
|
}
|
|
1730
1747
|
});
|
|
1731
1748
|
|
|
@@ -3292,6 +3309,13 @@ function constructMinimalModel(provider, modelId, _apiKey) {
|
|
|
3292
3309
|
// Provider-specific base URLs are handled by pi-ai internal registry
|
|
3293
3310
|
reasoning: false,
|
|
3294
3311
|
input: ["text"],
|
|
3312
|
+
// No catalog is available on this path, so there is no price data and no real
|
|
3313
|
+
// context/output ceiling to read. The zeros below are "unknown", NOT "free":
|
|
3314
|
+
// cost reporting for this model will read $0.00 (see warnIfUnpriced in
|
|
3315
|
+
// utils/llm-usage.ts), and the window figures are conservative guesses, not the
|
|
3316
|
+
// model's true limits — so docs/CONFIGURATION.md's "clamped to the model's real
|
|
3317
|
+
// ceiling" guarantee does not hold here. Anyone relying on accurate cost or the
|
|
3318
|
+
// full context window should give pi a credential so it can fetch the real catalog.
|
|
3295
3319
|
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
3296
3320
|
contextWindow: 128e3,
|
|
3297
3321
|
maxTokens: 32768,
|
|
@@ -3309,7 +3333,7 @@ function resolveModel(registry, modelSpec, provider, apiKey) {
|
|
|
3309
3333
|
if (apiKey && provider) {
|
|
3310
3334
|
return constructMinimalModel(prov, modelId, apiKey);
|
|
3311
3335
|
}
|
|
3312
|
-
throw new Error(
|
|
3336
|
+
throw new Error(buildModelNotFoundMessage(modelSpec));
|
|
3313
3337
|
}
|
|
3314
3338
|
const allModels = safeGetAll(registry);
|
|
3315
3339
|
const sameId = allModels.filter((m) => m.id === modelSpec);
|
|
@@ -3340,9 +3364,23 @@ function resolveModel(registry, modelSpec, provider, apiKey) {
|
|
|
3340
3364
|
}
|
|
3341
3365
|
return constructMinimalModel(provider, modelSpec, apiKey);
|
|
3342
3366
|
}
|
|
3343
|
-
throw new Error(
|
|
3344
|
-
|
|
3345
|
-
|
|
3367
|
+
throw new Error(buildNoModelAvailableMessage());
|
|
3368
|
+
}
|
|
3369
|
+
function credentialGuidance() {
|
|
3370
|
+
const agentDir = getAgentDir();
|
|
3371
|
+
return ` \u2022 Provider pi supports built-in (openrouter, anthropic, openai, \u2026): add a credential \u2014 run \`pi\` and use /login, set the provider's API-key environment variable, or add it to ${path6.join(agentDir, "auth.json")}. pi then fetches that provider's model catalog automatically.
|
|
3372
|
+
\u2022 Custom or self-hosted provider (Ollama, vLLM, a proxy): declare it in ${path6.join(agentDir, "models.json")}.
|
|
3373
|
+
Note: a models.json entry REPLACES pi's catalog entry for the same model id, discarding its pricing and
|
|
3374
|
+
context/output limits. To adjust a single field of a built-in model, use "modelOverrides" instead \u2014 it merges.`;
|
|
3375
|
+
}
|
|
3376
|
+
function buildModelNotFoundMessage(modelSpec) {
|
|
3377
|
+
return `Model "${modelSpec}" not found in pi's model registry.
|
|
3378
|
+
${credentialGuidance()}`;
|
|
3379
|
+
}
|
|
3380
|
+
function buildNoModelAvailableMessage() {
|
|
3381
|
+
return `No LLM model available.
|
|
3382
|
+
${credentialGuidance()}
|
|
3383
|
+
\u2022 Or pass an explicit apiKey.`;
|
|
3346
3384
|
}
|
|
3347
3385
|
function readModelsJsonProviderOrder() {
|
|
3348
3386
|
const modelsJsonPath = path6.join(getAgentDir(), "models.json");
|
|
@@ -3428,6 +3466,7 @@ function withTimeout2(promise, timeoutMs, label, signal) {
|
|
|
3428
3466
|
logger.debug(`[withTimeout] Starting ${timeoutMs}ms timeout guard (external signal: ${signal?.aborted ?? "no signal"})`);
|
|
3429
3467
|
return new Promise((resolve8, reject) => {
|
|
3430
3468
|
if (combinedSignal.aborted) {
|
|
3469
|
+
void promise.catch(() => void 0);
|
|
3431
3470
|
logger.debug(`[withTimeout] ${label} already aborted at start (external cancellation)`);
|
|
3432
3471
|
return reject(new Error(`${label} cancelled or timed out`));
|
|
3433
3472
|
}
|
|
@@ -3533,7 +3572,7 @@ async function retryWithBackoff(fn, options = {}) {
|
|
|
3533
3572
|
metrics.observe("retry_backoff_delay_ms", delay2, { attempt: String(attempt), label: opts.label });
|
|
3534
3573
|
logger.warn(`[Retry] ${opts.label} failed (attempt ${attempt + 1}/${opts.maxRetries + 1}), retrying in ${Math.round(delay2)}ms: ${lastError.message}`);
|
|
3535
3574
|
try {
|
|
3536
|
-
await abortableDelay(delay2, opts.signal);
|
|
3575
|
+
await abortableDelay(delay2, opts.signal, true);
|
|
3537
3576
|
} catch {
|
|
3538
3577
|
metrics.increment("retry_aborted_total", 1, { label: opts.label });
|
|
3539
3578
|
throw lastError;
|
|
@@ -4052,6 +4091,11 @@ var init_circuit_breaker = __esm({
|
|
|
4052
4091
|
metrics.observe("circuit_breaker_call_duration_ms", duration, { breaker: this.options.name, status: "error" });
|
|
4053
4092
|
if (this.options.isTransientError(error)) {
|
|
4054
4093
|
this.onFailure(error);
|
|
4094
|
+
} else if (admittedAsProbe && this.state === "HALF_OPEN") {
|
|
4095
|
+
logger.warn(
|
|
4096
|
+
`[CircuitBreaker] '${this.options.name}' HALF_OPEN probe failed with a non-counted error; treating as inconclusive and re-opening: ${error instanceof Error ? error.message : String(error)}`
|
|
4097
|
+
);
|
|
4098
|
+
this.transitionTo("OPEN");
|
|
4055
4099
|
}
|
|
4056
4100
|
throw error;
|
|
4057
4101
|
} finally {
|
|
@@ -8039,7 +8083,7 @@ function resolveResearchModel(options) {
|
|
|
8039
8083
|
if (anyModel.length > 0) {
|
|
8040
8084
|
return pickPreferredAvailable(anyModel, readModelsJsonProviderOrder());
|
|
8041
8085
|
}
|
|
8042
|
-
throw new Error(
|
|
8086
|
+
throw new Error(buildNoModelAvailableMessage());
|
|
8043
8087
|
}
|
|
8044
8088
|
var init_research_model_resolver = __esm({
|
|
8045
8089
|
"src/core/llm/research-model-resolver.ts"() {
|
|
@@ -8341,7 +8385,7 @@ async function runBackgroundExtraction(model, auth, conversationHistory, queries
|
|
|
8341
8385
|
const delay2 = KNOWLEDGE_SYNTHESIS_RETRY_BASE_MS * attempt;
|
|
8342
8386
|
metrics.increment("research_knowledge_search_synthesis_retries_total", 1);
|
|
8343
8387
|
logger.warn(`[research-knowledge-search] Synthesis attempt ${attempt}/${KNOWLEDGE_SYNTHESIS_MAX_ATTEMPTS} failed transiently (${msg2}); retrying in ${delay2}ms`);
|
|
8344
|
-
await abortableDelay(delay2, signal);
|
|
8388
|
+
await abortableDelay(delay2, signal, true);
|
|
8345
8389
|
}
|
|
8346
8390
|
}
|
|
8347
8391
|
const extracted = extractJson(responseText, "object");
|
|
@@ -8429,7 +8473,7 @@ ${desc}`;
|
|
|
8429
8473
|
const delay2 = KNOWLEDGE_SYNTHESIS_RETRY_BASE_MS * attempt;
|
|
8430
8474
|
metrics.increment("research_knowledge_search_triage_retries_total", 1);
|
|
8431
8475
|
logger.warn(`[research-knowledge-search] Triage attempt ${attempt}/${KNOWLEDGE_SYNTHESIS_MAX_ATTEMPTS} failed transiently (${msg2}); retrying in ${delay2}ms`);
|
|
8432
|
-
await abortableDelay(delay2, signal);
|
|
8476
|
+
await abortableDelay(delay2, signal, true);
|
|
8433
8477
|
}
|
|
8434
8478
|
}
|
|
8435
8479
|
const extracted = extractJson(responseText, "object");
|
|
@@ -9557,11 +9601,11 @@ function normalizeCitations(reports) {
|
|
|
9557
9601
|
});
|
|
9558
9602
|
const headerIdx = lastCitedLinksHeaderIndex(report);
|
|
9559
9603
|
let content = headerIdx >= 0 ? report.slice(0, headerIdx) : report;
|
|
9560
|
-
content =
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9604
|
+
content = rewriteCitationMarkers(
|
|
9605
|
+
content,
|
|
9606
|
+
localToGlobal,
|
|
9607
|
+
(n) => !localToGlobal.has(n)
|
|
9608
|
+
);
|
|
9565
9609
|
normalizedReports.set(id, content.trim());
|
|
9566
9610
|
}
|
|
9567
9611
|
return { normalizedReports, globalCitations };
|
|
@@ -9576,6 +9620,42 @@ function formatCitedLinks(citations) {
|
|
|
9576
9620
|
return `CITED LINKS
|
|
9577
9621
|
${links.join("\n")}`;
|
|
9578
9622
|
}
|
|
9623
|
+
var MAX_PLAUSIBLE_CITATION_MARKER = 200;
|
|
9624
|
+
function rewriteCitationMarkers(text, remap, isDangling) {
|
|
9625
|
+
const inFence = fencedCodeMask(text);
|
|
9626
|
+
return text.replace(/([ \t]*)\[(\d+)\]/g, (match, pad, digits, offset) => {
|
|
9627
|
+
const n = parseInt(digits, 10);
|
|
9628
|
+
const mapped = remap.get(n);
|
|
9629
|
+
if (mapped !== void 0) return `${pad}[${mapped}]`;
|
|
9630
|
+
if (n < 1 || n > MAX_PLAUSIBLE_CITATION_MARKER || !isDangling(n)) return match;
|
|
9631
|
+
if (inFence(offset) || inInlineCodeSpan(text, offset)) return match;
|
|
9632
|
+
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
|
9633
|
+
return text.slice(lineStart, offset).trim() === "" ? pad : "";
|
|
9634
|
+
});
|
|
9635
|
+
}
|
|
9636
|
+
function fencedCodeMask(text) {
|
|
9637
|
+
const ranges = [];
|
|
9638
|
+
let open4 = -1;
|
|
9639
|
+
let pos = 0;
|
|
9640
|
+
for (const line of text.split("\n")) {
|
|
9641
|
+
if (/^\s{0,3}(```|~~~)/.test(line)) {
|
|
9642
|
+
if (open4 < 0) open4 = pos;
|
|
9643
|
+
else {
|
|
9644
|
+
ranges.push([open4, pos + line.length]);
|
|
9645
|
+
open4 = -1;
|
|
9646
|
+
}
|
|
9647
|
+
}
|
|
9648
|
+
pos += line.length + 1;
|
|
9649
|
+
}
|
|
9650
|
+
if (open4 >= 0) ranges.push([open4, text.length]);
|
|
9651
|
+
return (offset) => ranges.some(([start, end]) => offset >= start && offset < end);
|
|
9652
|
+
}
|
|
9653
|
+
function inInlineCodeSpan(text, offset) {
|
|
9654
|
+
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
|
9655
|
+
let ticks = 0;
|
|
9656
|
+
for (let i = lineStart; i < offset; i++) if (text[i] === "`") ticks++;
|
|
9657
|
+
return ticks % 2 === 1;
|
|
9658
|
+
}
|
|
9579
9659
|
|
|
9580
9660
|
// src/core/planning-service.ts
|
|
9581
9661
|
init_agentic_repair();
|
|
@@ -10813,7 +10893,12 @@ function isServerDrainingError(error) {
|
|
|
10813
10893
|
function isPoolShutdownError(error) {
|
|
10814
10894
|
if (!error || typeof error !== "object") return false;
|
|
10815
10895
|
const err = error;
|
|
10816
|
-
return typeof err.message === "string" && (err.message.includes("Worker pool is shutting down") || err.message.includes("Cannot execute a task on destroying pool") || err.message.includes("destroying pool")
|
|
10896
|
+
return typeof err.message === "string" && (err.message.includes("Worker pool is shutting down") || err.message.includes("Cannot execute a task on destroying pool") || err.message.includes("destroying pool") || // worker-pool-manager throws this when auto-recovery is still swapping the
|
|
10897
|
+
// pool. Its own text says "please retry", but until it was listed here no
|
|
10898
|
+
// classifier matched it, so the first attempt rethrew instead of retrying —
|
|
10899
|
+
// and auto-recovery fires only after 3 consecutive worker failures, i.e.
|
|
10900
|
+
// exactly when a burst of tasks is in flight and would all die together.
|
|
10901
|
+
err.message.includes("Worker pool is being reset"));
|
|
10817
10902
|
}
|
|
10818
10903
|
var sessionCircuitBreakers = /* @__PURE__ */ new Map();
|
|
10819
10904
|
var MAX_SESSION_BREAKERS = 256;
|
|
@@ -14228,14 +14313,14 @@ init_file_lock_service();
|
|
|
14228
14313
|
init_service_registry();
|
|
14229
14314
|
init_service_names();
|
|
14230
14315
|
import * as pathmod2 from "node:path";
|
|
14231
|
-
var DEFAULT_MAX_CONCURRENT_RUNS =
|
|
14232
|
-
var DEFAULT_ACQUIRE_TIMEOUT_MS =
|
|
14316
|
+
var DEFAULT_MAX_CONCURRENT_RUNS = 3;
|
|
14317
|
+
var DEFAULT_ACQUIRE_TIMEOUT_MS = 6e5;
|
|
14233
14318
|
var POLL_DELAY_MS = 250;
|
|
14234
14319
|
var ResearchRunCapacityError = class extends Error {
|
|
14235
14320
|
slots;
|
|
14236
14321
|
constructor(slots, maxWaitMs) {
|
|
14237
14322
|
super(
|
|
14238
|
-
`Maximum concurrent research runs (${slots}) reached${maxWaitMs > 0 ? ` after
|
|
14323
|
+
`Maximum concurrent research runs (${slots}) reached${maxWaitMs > 0 ? ` and still busy after queueing for ${Math.round(maxWaitMs / 1e3)}s` : ""}; another run is still in progress. This is temporary \u2014 retry shortly. (Raise the cap with PI_RESEARCH_MAX_CONCURRENT_RUNS, or the queue wait with PI_RESEARCH_RUN_ACQUIRE_TIMEOUT_MS.)`
|
|
14239
14324
|
);
|
|
14240
14325
|
this.name = "ResearchRunCapacityError";
|
|
14241
14326
|
this.slots = slots;
|
|
@@ -14335,16 +14420,20 @@ var ResearchRunSemaphore = class {
|
|
|
14335
14420
|
* @param signal aborts the wait. Without it a run started under a long
|
|
14336
14421
|
* `PI_RESEARCH_RUN_ACQUIRE_TIMEOUT_MS` would ignore Ctrl-C until the deadline
|
|
14337
14422
|
* passed, because the wait happens before any orchestrator exists to cancel.
|
|
14423
|
+
* @param onQueued invoked at most once, when the first full sweep finds every
|
|
14424
|
+
* slot busy and the call is about to wait. Lets a front-end tell the user the
|
|
14425
|
+
* run is queued rather than hung. Never allowed to break the acquire.
|
|
14338
14426
|
* @throws {ResearchRunCapacityError} if no slot frees within `maxWaitMs`.
|
|
14339
14427
|
* @throws {Error} named `AbortError` if `signal` aborts while waiting.
|
|
14340
14428
|
*/
|
|
14341
|
-
async acquire(maxWaitMs = this.defaultMaxWaitMs, signal) {
|
|
14429
|
+
async acquire(maxWaitMs = this.defaultMaxWaitMs, signal, onQueued) {
|
|
14342
14430
|
if (!this._initialized) await this.initialize();
|
|
14343
14431
|
if (signal?.aborted) {
|
|
14344
14432
|
metrics.increment("research_run_acquire_total", 1, { status: "aborted" });
|
|
14345
14433
|
throwIfAborted(signal);
|
|
14346
14434
|
}
|
|
14347
14435
|
const deadline = Date.now() + Math.max(0, maxWaitMs);
|
|
14436
|
+
let announcedQueued = false;
|
|
14348
14437
|
for (; ; ) {
|
|
14349
14438
|
const start = this.nextStartIndex;
|
|
14350
14439
|
this.nextStartIndex = (this.nextStartIndex + 1) % this.maxSlots;
|
|
@@ -14361,6 +14450,15 @@ var ResearchRunSemaphore = class {
|
|
|
14361
14450
|
metrics.increment("research_run_acquire_total", 1, { status: "exhausted" });
|
|
14362
14451
|
throw new ResearchRunCapacityError(this.maxSlots, maxWaitMs);
|
|
14363
14452
|
}
|
|
14453
|
+
if (!announcedQueued) {
|
|
14454
|
+
announcedQueued = true;
|
|
14455
|
+
logger.info(`[ResearchRunSemaphore] All ${this.maxSlots} run slot(s) busy \u2014 queueing for up to ${maxWaitMs}ms.`);
|
|
14456
|
+
try {
|
|
14457
|
+
onQueued?.(this.maxSlots, maxWaitMs);
|
|
14458
|
+
} catch (err) {
|
|
14459
|
+
logger.debug(`[ResearchRunSemaphore] onQueued callback threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
14460
|
+
}
|
|
14461
|
+
}
|
|
14364
14462
|
try {
|
|
14365
14463
|
await sleep(POLL_DELAY_MS, signal);
|
|
14366
14464
|
} catch (err) {
|
|
@@ -14886,6 +14984,17 @@ init_service_registry();
|
|
|
14886
14984
|
init_service_interfaces();
|
|
14887
14985
|
var lastRestartTime = 0;
|
|
14888
14986
|
var RESTART_COOLDOWN_MS = 1e4;
|
|
14987
|
+
var LEADER_HANDOVER_RETRIES = 3;
|
|
14988
|
+
var HANDOVER_BACKOFF_MS = 1200;
|
|
14989
|
+
async function recoverFromLeaderHandover(container) {
|
|
14990
|
+
logger.warn("[BrowserManager] Leader handover detected (pool draining) \u2014 dropping stale scheduler handle and re-resolving...");
|
|
14991
|
+
await forceSchedulerRestart2(false, container).catch(
|
|
14992
|
+
(err) => logger.debug("[BrowserManager] Scheduler re-resolve during handover failed (non-fatal):", err)
|
|
14993
|
+
);
|
|
14994
|
+
await waitForBrowserPoolIdle(15e3).catch(
|
|
14995
|
+
(err) => logger.debug("Wait for browser idle timed out or failed:", err)
|
|
14996
|
+
);
|
|
14997
|
+
}
|
|
14889
14998
|
async function getScheduler2(config, container = getServiceContainer()) {
|
|
14890
14999
|
try {
|
|
14891
15000
|
const factory = await getService(ServiceNames.SCHEDULER_FACTORY, void 0, container);
|
|
@@ -14902,7 +15011,7 @@ async function forceSchedulerRestart2(forceClearRemoteState = false, container =
|
|
|
14902
15011
|
return await forceSchedulerRestart(forceClearRemoteState, container);
|
|
14903
15012
|
}
|
|
14904
15013
|
}
|
|
14905
|
-
async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retries = 1, container = getServiceContainer()) {
|
|
15014
|
+
async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retries = 1, container = getServiceContainer(), handoverRetries = LEADER_HANDOVER_RETRIES) {
|
|
14906
15015
|
if (signal?.aborted) throw new Error("Aborted");
|
|
14907
15016
|
const sessionId = typeof taskOrUrl === "object" ? taskOrUrl.sessionId : void 0;
|
|
14908
15017
|
const breaker2 = sessionId ? getBrowserCircuitBreaker(sessionId) : browserCircuitBreaker;
|
|
@@ -14923,6 +15032,19 @@ async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retrie
|
|
|
14923
15032
|
});
|
|
14924
15033
|
} catch (error) {
|
|
14925
15034
|
if (signal?.aborted || error instanceof Error && error.message === "Aborted") throw new Error("Aborted", { cause: error });
|
|
15035
|
+
const isHandover = isPoolShutdownError(error) && !isTaskTimeoutError(error) && !isCloudflareBlockError(error);
|
|
15036
|
+
if (isHandover && handoverRetries > 0) {
|
|
15037
|
+
errorTracker.trackError(error, {
|
|
15038
|
+
component: "browser-manager",
|
|
15039
|
+
operation: type,
|
|
15040
|
+
taskType: type,
|
|
15041
|
+
errorType: "leader_handover"
|
|
15042
|
+
});
|
|
15043
|
+
logger.warn(`[BrowserManager] Leader handover during ${type} task (handover attempts left: ${handoverRetries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
|
|
15044
|
+
await recoverFromLeaderHandover(container);
|
|
15045
|
+
await new Promise((resolve8) => setTimeout(resolve8, HANDOVER_BACKOFF_MS + Math.floor(Math.random() * 400)));
|
|
15046
|
+
return runBrowserTask(taskOrUrl, type, config, signal, retries, container, handoverRetries - 1);
|
|
15047
|
+
}
|
|
14926
15048
|
if (retries > 0 && isTransientSocketError(error) && !isTaskTimeoutError(error) && !isCloudflareBlockError(error)) {
|
|
14927
15049
|
errorTracker.trackError(error, {
|
|
14928
15050
|
component: "browser-manager",
|
|
@@ -14948,7 +15070,7 @@ async function runBrowserTask(taskOrUrl, type = "scrape", config, signal, retrie
|
|
|
14948
15070
|
}
|
|
14949
15071
|
const jitter = 100 + Math.floor(Math.random() * 400);
|
|
14950
15072
|
await new Promise((resolve8) => setTimeout(resolve8, jitter));
|
|
14951
|
-
return runBrowserTask(taskOrUrl, type, config, signal, retries - 1, container);
|
|
15073
|
+
return runBrowserTask(taskOrUrl, type, config, signal, retries - 1, container, handoverRetries);
|
|
14952
15074
|
}
|
|
14953
15075
|
throw error;
|
|
14954
15076
|
}
|
|
@@ -14968,7 +15090,7 @@ async function runBrowserHealthCheck(config, retries = 1, signal, container = ge
|
|
|
14968
15090
|
});
|
|
14969
15091
|
logger.warn(`[BrowserManager] Transient socket error during healthcheck (retries left: ${retries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
|
|
14970
15092
|
if (isPoolShutdownError(error)) {
|
|
14971
|
-
await
|
|
15093
|
+
await recoverFromLeaderHandover(container);
|
|
14972
15094
|
} else {
|
|
14973
15095
|
const now = Date.now();
|
|
14974
15096
|
if (now - lastRestartTime > RESTART_COOLDOWN_MS) {
|
|
@@ -14987,7 +15109,7 @@ async function runBrowserHealthCheck(config, retries = 1, signal, container = ge
|
|
|
14987
15109
|
throw error;
|
|
14988
15110
|
}
|
|
14989
15111
|
}
|
|
14990
|
-
async function runWorkerSearch(query, config, signal, retries = 1, sessionId, container = getServiceContainer()) {
|
|
15112
|
+
async function runWorkerSearch(query, config, signal, retries = 1, sessionId, container = getServiceContainer(), handoverRetries = LEADER_HANDOVER_RETRIES) {
|
|
14991
15113
|
if (signal?.aborted) throw new Error("Aborted");
|
|
14992
15114
|
const breaker2 = sessionId ? getBrowserCircuitBreaker(sessionId) : browserCircuitBreaker;
|
|
14993
15115
|
try {
|
|
@@ -14998,6 +15120,19 @@ async function runWorkerSearch(query, config, signal, retries = 1, sessionId, co
|
|
|
14998
15120
|
});
|
|
14999
15121
|
} catch (error) {
|
|
15000
15122
|
if (signal?.aborted || error instanceof Error && error.message === "Aborted") throw new Error("Aborted", { cause: error });
|
|
15123
|
+
const isHandover = isPoolShutdownError(error) && !isTaskTimeoutError(error) && !isCloudflareBlockError(error);
|
|
15124
|
+
if (isHandover && handoverRetries > 0) {
|
|
15125
|
+
errorTracker.trackError(error, {
|
|
15126
|
+
component: "browser-manager",
|
|
15127
|
+
operation: "search",
|
|
15128
|
+
query,
|
|
15129
|
+
errorType: "leader_handover"
|
|
15130
|
+
});
|
|
15131
|
+
logger.warn(`[BrowserManager] Leader handover during search (handover attempts left: ${handoverRetries}): ${(error instanceof Error ? error.message : String(error)).substring(0, 100)}...`);
|
|
15132
|
+
await recoverFromLeaderHandover(container);
|
|
15133
|
+
await new Promise((resolve8) => setTimeout(resolve8, HANDOVER_BACKOFF_MS + Math.floor(Math.random() * 400)));
|
|
15134
|
+
return runWorkerSearch(query, config, signal, retries, sessionId, container, handoverRetries - 1);
|
|
15135
|
+
}
|
|
15001
15136
|
if (retries > 0 && isTransientSocketError(error) && !isTaskTimeoutError(error) && !isCloudflareBlockError(error)) {
|
|
15002
15137
|
errorTracker.trackError(error, {
|
|
15003
15138
|
component: "browser-manager",
|
|
@@ -15021,7 +15156,7 @@ async function runWorkerSearch(query, config, signal, retries = 1, sessionId, co
|
|
|
15021
15156
|
}
|
|
15022
15157
|
const jitter = 100 + Math.floor(Math.random() * 400);
|
|
15023
15158
|
await new Promise((resolve8) => setTimeout(resolve8, jitter));
|
|
15024
|
-
return runWorkerSearch(query, config, signal, retries - 1, sessionId, container);
|
|
15159
|
+
return runWorkerSearch(query, config, signal, retries - 1, sessionId, container, handoverRetries);
|
|
15025
15160
|
}
|
|
15026
15161
|
throw error;
|
|
15027
15162
|
}
|
|
@@ -20813,6 +20948,9 @@ var HeadlessObserver = class {
|
|
|
20813
20948
|
onStart(query, complexity) {
|
|
20814
20949
|
this.emit("start", { query, complexity });
|
|
20815
20950
|
}
|
|
20951
|
+
onRunQueued(slots, maxWaitMs) {
|
|
20952
|
+
this.emit("run_queued", { slots, maxWaitMs });
|
|
20953
|
+
}
|
|
20816
20954
|
onPlanningStart(attempt) {
|
|
20817
20955
|
this.emit("planning_start", { attempt });
|
|
20818
20956
|
}
|
|
@@ -21611,7 +21749,9 @@ var ResearchOrchestrationService = class {
|
|
|
21611
21749
|
try {
|
|
21612
21750
|
const container = tryGetServiceContainerFromCtx(ctx);
|
|
21613
21751
|
const semaphore = await getService(ServiceNames.RESEARCH_RUN_SEMAPHORE, ctx, container);
|
|
21614
|
-
runSlot = await semaphore.acquire(void 0, signal)
|
|
21752
|
+
runSlot = await semaphore.acquire(void 0, signal, (slots, maxWaitMs) => {
|
|
21753
|
+
notifyRunQueued(observer, slots, maxWaitMs);
|
|
21754
|
+
});
|
|
21615
21755
|
logger.info(`[ResearchOrchestrationService] Acquired research run slot ${runSlot.slotIndex} (cap ${semaphore.getMaxSlots()}).`);
|
|
21616
21756
|
} catch (err) {
|
|
21617
21757
|
if (err instanceof ResearchRunCapacityError) throw err;
|
|
@@ -21965,6 +22105,15 @@ var ResearchOrchestrationService = class {
|
|
|
21965
22105
|
}
|
|
21966
22106
|
}
|
|
21967
22107
|
};
|
|
22108
|
+
function notifyRunQueued(observer, slots, maxWaitMs) {
|
|
22109
|
+
try {
|
|
22110
|
+
const obs = observer;
|
|
22111
|
+
if (typeof obs?.onRunQueued === "function") obs.onRunQueued(slots, maxWaitMs);
|
|
22112
|
+
else if (typeof obs?.onProgress === "function") obs.onProgress("run_queued", { slots, maxWaitMs });
|
|
22113
|
+
} catch (err) {
|
|
22114
|
+
logger.debug(`[ResearchOrchestrationService] run_queued notification threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
22115
|
+
}
|
|
22116
|
+
}
|
|
21968
22117
|
|
|
21969
22118
|
// src/orchestration/research-session-service.ts
|
|
21970
22119
|
init_logger();
|
|
@@ -22114,42 +22263,6 @@ init_service_registry();
|
|
|
22114
22263
|
init_service_names();
|
|
22115
22264
|
var NO_SOURCES_NOTE = "CITED LINKS\n_No web sources were successfully retrieved for this report \u2014 searches or page fetches may have been blocked (for example by site bot-protection) or returned no usable results. Treat the findings above as unverified._";
|
|
22116
22265
|
var UNVERIFIED_LINKS_NOTE = "_None of the links above could be verified: no page fetch succeeded this session and no citation could be matched to retrieved content. They may be inaccurate or invented \u2014 check each one before relying on it._";
|
|
22117
|
-
var MAX_PLAUSIBLE_CITATION_MARKER = 200;
|
|
22118
|
-
function rewriteCitationMarkers(text, remap, isDangling) {
|
|
22119
|
-
const inFence = fencedCodeMask(text);
|
|
22120
|
-
return text.replace(/([ \t]*)\[(\d+)\]/g, (match, pad, digits, offset) => {
|
|
22121
|
-
const n = parseInt(digits, 10);
|
|
22122
|
-
const mapped = remap.get(n);
|
|
22123
|
-
if (mapped !== void 0) return `${pad}[${mapped}]`;
|
|
22124
|
-
if (n < 1 || n > MAX_PLAUSIBLE_CITATION_MARKER || !isDangling(n)) return match;
|
|
22125
|
-
if (inFence(offset) || inInlineCodeSpan(text, offset)) return match;
|
|
22126
|
-
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
|
22127
|
-
return text.slice(lineStart, offset).trim() === "" ? pad : "";
|
|
22128
|
-
});
|
|
22129
|
-
}
|
|
22130
|
-
function fencedCodeMask(text) {
|
|
22131
|
-
const ranges = [];
|
|
22132
|
-
let open4 = -1;
|
|
22133
|
-
let pos = 0;
|
|
22134
|
-
for (const line of text.split("\n")) {
|
|
22135
|
-
if (/^\s{0,3}(```|~~~)/.test(line)) {
|
|
22136
|
-
if (open4 < 0) open4 = pos;
|
|
22137
|
-
else {
|
|
22138
|
-
ranges.push([open4, pos + line.length]);
|
|
22139
|
-
open4 = -1;
|
|
22140
|
-
}
|
|
22141
|
-
}
|
|
22142
|
-
pos += line.length + 1;
|
|
22143
|
-
}
|
|
22144
|
-
if (open4 >= 0) ranges.push([open4, text.length]);
|
|
22145
|
-
return (offset) => ranges.some(([start, end]) => offset >= start && offset < end);
|
|
22146
|
-
}
|
|
22147
|
-
function inInlineCodeSpan(text, offset) {
|
|
22148
|
-
const lineStart = text.lastIndexOf("\n", offset - 1) + 1;
|
|
22149
|
-
let ticks = 0;
|
|
22150
|
-
for (let i = lineStart; i < offset; i++) if (text[i] === "`") ticks++;
|
|
22151
|
-
return ticks % 2 === 1;
|
|
22152
|
-
}
|
|
22153
22266
|
function redactUnverifiedProseUrls(text, trusted) {
|
|
22154
22267
|
if (trusted.size === 0) return text;
|
|
22155
22268
|
let removed = 0;
|
|
@@ -23191,8 +23304,8 @@ function appendExportMessage(result, filepath, totalCost) {
|
|
|
23191
23304
|
---
|
|
23192
23305
|
|
|
23193
23306
|
Research report saved to: ${filepath}`;
|
|
23194
|
-
if (totalCost !== void 0
|
|
23195
|
-
const costStr = totalCost < 0.01 ? "<$0.01" : `$${totalCost.toFixed(4)}`.replace(/0+$/, "").replace(/\.$/, "");
|
|
23307
|
+
if (totalCost !== void 0) {
|
|
23308
|
+
const costStr = totalCost === 0 ? "$0.00" : totalCost < 0.01 ? "<$0.01" : `$${totalCost.toFixed(4)}`.replace(/0+$/, "").replace(/\.$/, "");
|
|
23196
23309
|
footer += `
|
|
23197
23310
|
Total cost: ${costStr}`;
|
|
23198
23311
|
}
|
|
@@ -23221,7 +23334,15 @@ var EXIT = {
|
|
|
23221
23334
|
/** Missing setup: package, model, or API key not configured. */
|
|
23222
23335
|
CONFIG: 78,
|
|
23223
23336
|
/** Something went wrong at runtime (network, provider, internal). */
|
|
23224
|
-
SOFTWARE: 70
|
|
23337
|
+
SOFTWARE: 70,
|
|
23338
|
+
/**
|
|
23339
|
+
* Temporary failure — the request was well-formed and the install is healthy,
|
|
23340
|
+
* but the machine is at capacity right now (all research run slots held by
|
|
23341
|
+
* live runs). Distinct from SOFTWARE so a calling agent can tell "busy, try
|
|
23342
|
+
* again shortly" apart from "this run crashed" and back off instead of
|
|
23343
|
+
* re-running a broken command.
|
|
23344
|
+
*/
|
|
23345
|
+
TEMPFAIL: 75
|
|
23225
23346
|
};
|
|
23226
23347
|
function toStderr(s) {
|
|
23227
23348
|
process.stderr.write(s);
|
|
@@ -23241,6 +23362,12 @@ function makeProgressObserver() {
|
|
|
23241
23362
|
toStderr(` \u2022 plan: ${data?.plan?.researchers?.length ?? "?"} researcher(s)
|
|
23242
23363
|
`);
|
|
23243
23364
|
break;
|
|
23365
|
+
case "run_queued":
|
|
23366
|
+
toStderr(
|
|
23367
|
+
` \u2022 queued: ${data?.slots ?? "?"} concurrent run(s) already active \u2014 waiting up to ${Math.round((data?.maxWaitMs ?? 0) / 1e3)}s for a slot\u2026
|
|
23368
|
+
`
|
|
23369
|
+
);
|
|
23370
|
+
break;
|
|
23244
23371
|
case "round_start":
|
|
23245
23372
|
toStderr(` \u2022 round ${data?.round}
|
|
23246
23373
|
`);
|
|
@@ -23261,10 +23388,16 @@ function makeProgressObserver() {
|
|
|
23261
23388
|
toStderr(` \u2022 evaluator: ${data?.action}
|
|
23262
23389
|
`);
|
|
23263
23390
|
break;
|
|
23264
|
-
case "tokens_consumed":
|
|
23265
|
-
|
|
23391
|
+
case "tokens_consumed": {
|
|
23392
|
+
const consumed = Number(data?.tokens ?? 0);
|
|
23393
|
+
const costVal = typeof data?.cost === "number" ? data.cost : void 0;
|
|
23394
|
+
if (consumed > 0 || costVal !== void 0) {
|
|
23395
|
+
const costPart = costVal === void 0 ? "" : ` ($${costVal.toFixed(4)})`;
|
|
23396
|
+
toStderr(` \u2022 tokens: ${consumed}${costPart}
|
|
23266
23397
|
`);
|
|
23398
|
+
}
|
|
23267
23399
|
break;
|
|
23400
|
+
}
|
|
23268
23401
|
case "complete":
|
|
23269
23402
|
toStderr(` \u2022 complete (${data?.result?.length ?? 0} chars)
|
|
23270
23403
|
`);
|
|
@@ -23785,17 +23918,25 @@ async function reportError(err, what, json) {
|
|
|
23785
23918
|
const msg2 = err instanceof Error ? err.message : String(err);
|
|
23786
23919
|
const lower = msg2.toLowerCase();
|
|
23787
23920
|
const isTaggedResearchStop = err?.code === RESEARCH_STOPPED_ERROR_CODE;
|
|
23788
|
-
const
|
|
23921
|
+
const isCapacity = err instanceof Error && err.name === "ResearchRunCapacityError";
|
|
23922
|
+
const isConfigError = !isTaggedResearchStop && !isCapacity && (lower.includes("no llm model available") || lower.includes("not found in pi") || lower.includes("no api key") || lower.includes("provider must be specified") || lower.includes("invalid model string") || lower.includes("authentication") || lower.includes("unauthorized") || lower.includes("api key"));
|
|
23789
23923
|
const isRateLimit = lower.includes("429") || lower.includes("rate limit") || lower.includes("too many requests");
|
|
23790
|
-
const exitCode = isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23924
|
+
const exitCode = isCapacity ? EXIT.TEMPFAIL : isConfigError && !isRateLimit ? EXIT.CONFIG : EXIT.SOFTWARE;
|
|
23791
23925
|
if (json) {
|
|
23792
23926
|
const payload = { ok: false, error: msg2, exitCode };
|
|
23927
|
+
if (isCapacity || isRateLimit) payload["retryable"] = true;
|
|
23793
23928
|
if (process.env["PI_RESEARCH_DEBUG"] === "true" && err instanceof Error && err.stack) {
|
|
23794
23929
|
payload["stack"] = err.stack;
|
|
23795
23930
|
}
|
|
23796
23931
|
toStdout(pretty(payload));
|
|
23797
23932
|
return exitCode;
|
|
23798
23933
|
}
|
|
23934
|
+
if (isCapacity) {
|
|
23935
|
+
toStderr(`
|
|
23936
|
+
Error: pi-research ${what} did not start: ${msg2}
|
|
23937
|
+
`);
|
|
23938
|
+
return exitCode;
|
|
23939
|
+
}
|
|
23799
23940
|
if (isRateLimit) {
|
|
23800
23941
|
toStderr(`
|
|
23801
23942
|
Error: pi-research ${what} halted: provider rate limit reached. Wait a moment and retry.
|
|
@@ -23898,7 +24039,9 @@ CONFIGURE
|
|
|
23898
24039
|
EXIT CODES
|
|
23899
24040
|
0 success
|
|
23900
24041
|
64 bad arguments
|
|
24042
|
+
75 at capacity \u2014 every concurrent run slot is busy; nothing is broken, retry later
|
|
23901
24043
|
78 not configured \u2014 message prints the exact locations to fix
|
|
24044
|
+
(on \`knowledge\`, also returned when the store is deliberately disabled)
|
|
23902
24045
|
70 runtime error (network, provider, internal)
|
|
23903
24046
|
`;
|
|
23904
24047
|
}
|
package/dist/thread-worker.mjs
CHANGED
|
@@ -1284,6 +1284,11 @@ var init_circuit_breaker = __esm({
|
|
|
1284
1284
|
metrics.observe("circuit_breaker_call_duration_ms", duration, { breaker: this.options.name, status: "error" });
|
|
1285
1285
|
if (this.options.isTransientError(error)) {
|
|
1286
1286
|
this.onFailure(error);
|
|
1287
|
+
} else if (admittedAsProbe && this.state === "HALF_OPEN") {
|
|
1288
|
+
logger.warn(
|
|
1289
|
+
`[CircuitBreaker] '${this.options.name}' HALF_OPEN probe failed with a non-counted error; treating as inconclusive and re-opening: ${error instanceof Error ? error.message : String(error)}`
|
|
1290
|
+
);
|
|
1291
|
+
this.transitionTo("OPEN");
|
|
1287
1292
|
}
|
|
1288
1293
|
throw error;
|
|
1289
1294
|
} finally {
|
|
@@ -1379,7 +1384,12 @@ function isServerDrainingError(error) {
|
|
|
1379
1384
|
function isPoolShutdownError(error) {
|
|
1380
1385
|
if (!error || typeof error !== "object") return false;
|
|
1381
1386
|
const err = error;
|
|
1382
|
-
return typeof err.message === "string" && (err.message.includes("Worker pool is shutting down") || err.message.includes("Cannot execute a task on destroying pool") || err.message.includes("destroying pool")
|
|
1387
|
+
return typeof err.message === "string" && (err.message.includes("Worker pool is shutting down") || err.message.includes("Cannot execute a task on destroying pool") || err.message.includes("destroying pool") || // worker-pool-manager throws this when auto-recovery is still swapping the
|
|
1388
|
+
// pool. Its own text says "please retry", but until it was listed here no
|
|
1389
|
+
// classifier matched it, so the first attempt rethrew instead of retrying —
|
|
1390
|
+
// and auto-recovery fires only after 3 consecutive worker failures, i.e.
|
|
1391
|
+
// exactly when a burst of tasks is in flight and would all die together.
|
|
1392
|
+
err.message.includes("Worker pool is being reset"));
|
|
1383
1393
|
}
|
|
1384
1394
|
var DEFAULT_BREAKER_CONFIG, browserCircuitBreaker;
|
|
1385
1395
|
var init_browser_error_utils = __esm({
|